From 32915b1d0a315598edb737785d0357b5a1b8aa11 Mon Sep 17 00:00:00 2001 From: "Arnaud Porterie (icecrime)" Date: Mon, 17 Apr 2017 18:18:46 -0500 Subject: [PATCH] Remove cmd/docker and other directories in cli/ in accordance with the new Moby project scope Starting with this commit, integration tests should no longer rely on the docker cli, they should be API tests instead. For the existing tests the scripts will use a frozen version of the docker cli with a DOCKER_API_VERSION frozen to 1.30, which should ensure that the CI remains green at all times. To help contributors develop and test manually with a modified docker cli, this commit also adds a DOCKER_CLI_PATH environment variable to the Makefile. This allows to set the path of a custom cli that will be available inside the development container and used to run the integration tests. Signed-off-by: Arnaud Porterie (icecrime) Signed-off-by: Tibor Vass --- Dockerfile | 5 +- Dockerfile.aarch64 | 3 +- Dockerfile.armhf | 3 +- Dockerfile.ppc64le | 3 +- Dockerfile.s390x | 3 +- Dockerfile.simple | 3 +- Makefile | 12 +- cli/cli.go | 25 + cli/command/bundlefile/bundlefile.go | 70 - cli/command/bundlefile/bundlefile_test.go | 77 -- cli/command/checkpoint/cmd.go | 24 - cli/command/checkpoint/create.go | 58 - cli/command/checkpoint/list.go | 54 - cli/command/checkpoint/remove.go | 44 - cli/command/cli.go | 310 ----- cli/command/commands/commands.go | 121 -- cli/command/container/attach.go | 129 -- cli/command/container/cmd.go | 45 - cli/command/container/commit.go | 75 -- cli/command/container/cp.go | 305 ----- cli/command/container/create.go | 224 ---- cli/command/container/diff.go | 46 - cli/command/container/exec.go | 205 --- cli/command/container/exec_test.go | 116 -- cli/command/container/export.go | 58 - cli/command/container/hijack.go | 124 -- cli/command/container/inspect.go | 46 - cli/command/container/kill.go | 56 - cli/command/container/list.go | 140 -- cli/command/container/logs.go | 76 -- cli/command/container/opts.go | 900 ------------- cli/command/container/opts_test.go | 870 ------------- cli/command/container/pause.go | 49 - cli/command/container/port.go | 78 -- cli/command/container/prune.go | 78 -- cli/command/container/ps_test.go | 118 -- cli/command/container/rename.go | 51 - cli/command/container/restart.go | 62 - cli/command/container/rm.go | 73 -- cli/command/container/run.go | 296 ----- cli/command/container/start.go | 179 --- cli/command/container/stats.go | 242 ---- cli/command/container/stats_helpers.go | 229 ---- cli/command/container/stats_unit_test.go | 20 - cli/command/container/stop.go | 67 - cli/command/container/testdata/utf16.env | Bin 54 -> 0 bytes cli/command/container/testdata/utf16be.env | Bin 54 -> 0 bytes cli/command/container/testdata/utf8.env | 3 - cli/command/container/testdata/valid.env | 1 - cli/command/container/testdata/valid.label | 1 - cli/command/container/top.go | 57 - cli/command/container/tty.go | 103 -- cli/command/container/unpause.go | 50 - cli/command/container/update.go | 134 -- cli/command/container/utils.go | 142 --- cli/command/container/wait.go | 50 - cli/command/events_utils.go | 49 - cli/command/formatter/checkpoint.go | 52 - cli/command/formatter/checkpoint_test.go | 55 - cli/command/formatter/container.go | 259 ---- cli/command/formatter/container_test.go | 385 ------ cli/command/formatter/custom.go | 35 - cli/command/formatter/custom_test.go | 28 - cli/command/formatter/diff.go | 72 -- cli/command/formatter/diff_test.go | 59 - cli/command/formatter/disk_usage.go | 358 ------ cli/command/formatter/disk_usage_test.go | 125 -- cli/command/formatter/formatter.go | 119 -- cli/command/formatter/history.go | 113 -- cli/command/formatter/history_test.go | 213 ---- cli/command/formatter/image.go | 272 ---- cli/command/formatter/image_test.go | 327 ----- cli/command/formatter/network.go | 129 -- cli/command/formatter/network_test.go | 213 ---- cli/command/formatter/node.go | 292 ----- cli/command/formatter/node_test.go | 188 --- cli/command/formatter/plugin.go | 95 -- cli/command/formatter/plugin_test.go | 182 --- cli/command/formatter/reflect.go | 66 - cli/command/formatter/reflect_test.go | 66 - cli/command/formatter/secret.go | 101 -- cli/command/formatter/secret_test.go | 63 - cli/command/formatter/service.go | 535 -------- cli/command/formatter/service_test.go | 239 ---- cli/command/formatter/stack.go | 67 - cli/command/formatter/stack_test.go | 64 - cli/command/formatter/stats.go | 220 ---- cli/command/formatter/stats_test.go | 266 ---- cli/command/formatter/task.go | 150 --- cli/command/formatter/task_test.go | 107 -- cli/command/formatter/volume.go | 131 -- cli/command/formatter/volume_test.go | 183 --- cli/command/idresolver/client_test.go | 28 - cli/command/idresolver/idresolver.go | 70 - cli/command/idresolver/idresolver_test.go | 144 --- cli/command/image/build.go | 558 -------- cli/command/image/build/context.go | 275 ---- cli/command/image/build/context_test.go | 383 ------ cli/command/image/build/context_unix.go | 11 - cli/command/image/build/context_windows.go | 17 - cli/command/image/client_test.go | 116 -- cli/command/image/cmd.go | 33 - cli/command/image/history.go | 64 - cli/command/image/history_test.go | 108 -- cli/command/image/import.go | 88 -- cli/command/image/import_test.go | 100 -- cli/command/image/inspect.go | 44 - cli/command/image/inspect_test.go | 92 -- cli/command/image/list.go | 96 -- cli/command/image/list_test.go | 102 -- cli/command/image/load.go | 77 -- cli/command/image/load_test.go | 105 -- cli/command/image/prune.go | 95 -- cli/command/image/prune_test.go | 100 -- cli/command/image/pull.go | 85 -- cli/command/image/pull_test.go | 85 -- cli/command/image/push.go | 61 - cli/command/image/push_test.go | 85 -- cli/command/image/remove.go | 78 -- cli/command/image/remove_test.go | 103 -- cli/command/image/save.go | 56 - cli/command/image/save_test.go | 100 -- cli/command/image/tag.go | 41 - cli/command/image/tag_test.go | 44 - ...tory-command-success.quiet-no-trunc.golden | 1 - .../history-command-success.quiet.golden | 1 - .../history-command-success.simple.golden | 2 - .../testdata/import-command-success.input.txt | 1 - .../inspect-command-success.format.golden | 1 - ...inspect-command-success.simple-many.golden | 50 - .../inspect-command-success.simple.golden | 26 - .../list-command-success.filters.golden | 1 - .../list-command-success.format.golden | 0 .../list-command-success.match-name.golden | 1 - .../list-command-success.quiet-format.golden | 0 .../list-command-success.simple.golden | 1 - .../load-command-success.input-file.golden | 1 - .../testdata/load-command-success.input.txt | 1 - .../testdata/load-command-success.json.golden | 1 - .../load-command-success.simple.golden | 1 - .../testdata/prune-command-success.all.golden | 2 - ...prune-command-success.force-deleted.golden | 4 - ...rune-command-success.force-untagged.golden | 4 - .../pull-command-success.simple-no-tag.golden | 1 - .../pull-command-success.simple.golden | 0 ...-success.Image Deleted and Untagged.golden | 4 - ...emove-command-success.Image Deleted.golden | 2 - ...move-command-success.Image Untagged.golden | 2 - cli/command/image/trust.go | 382 ------ cli/command/image/trust_test.go | 57 - cli/command/in.go | 56 - cli/command/inspect/inspector.go | 198 --- cli/command/inspect/inspector_test.go | 221 ---- cli/command/network/cmd.go | 28 - cli/command/network/connect.go | 63 - cli/command/network/create.go | 232 ---- cli/command/network/disconnect.go | 41 - cli/command/network/inspect.go | 47 - cli/command/network/list.go | 76 -- cli/command/network/prune.go | 77 -- cli/command/network/remove.go | 53 - cli/command/node/client_test.go | 68 - cli/command/node/cmd.go | 57 - cli/command/node/demote.go | 36 - cli/command/node/demote_test.go | 89 -- cli/command/node/inspect.go | 72 -- cli/command/node/inspect_test.go | 124 -- cli/command/node/list.go | 73 -- cli/command/node/list_test.go | 162 --- cli/command/node/opts.go | 24 - cli/command/node/promote.go | 36 - cli/command/node/promote_test.go | 89 -- cli/command/node/ps.go | 109 -- cli/command/node/ps_test.go | 134 -- cli/command/node/remove.go | 57 - cli/command/node/remove_test.go | 48 - .../node-inspect-pretty.manager-leader.golden | 25 - .../node-inspect-pretty.manager.golden | 25 - .../node-inspect-pretty.simple.golden | 23 - .../node/testdata/node-ps.simple.golden | 2 - .../node/testdata/node-ps.with-errors.golden | 4 - cli/command/node/update.go | 121 -- cli/command/node/update_test.go | 173 --- cli/command/out.go | 50 - cli/command/plugin/cmd.go | 32 - cli/command/plugin/create.go | 128 -- cli/command/plugin/disable.go | 36 - cli/command/plugin/enable.go | 48 - cli/command/plugin/inspect.go | 42 - cli/command/plugin/install.go | 168 --- cli/command/plugin/list.go | 63 - cli/command/plugin/push.go | 69 - cli/command/plugin/remove.go | 55 - cli/command/plugin/set.go | 22 - cli/command/plugin/upgrade.go | 90 -- cli/command/prune/prune.go | 51 - cli/command/registry.go | 187 --- cli/command/registry/login.go | 87 -- cli/command/registry/logout.go | 77 -- cli/command/registry/search.go | 126 -- cli/command/secret/client_test.go | 44 - cli/command/secret/cmd.go | 26 - cli/command/secret/create.go | 80 -- cli/command/secret/create_test.go | 127 -- cli/command/secret/inspect.go | 41 - cli/command/secret/inspect_test.go | 150 --- cli/command/secret/ls.go | 61 - cli/command/secret/ls_test.go | 173 --- cli/command/secret/remove.go | 53 - cli/command/secret/remove_test.go | 82 -- .../testdata/secret-create-with-name.golden | 1 - ...t-inspect-with-format.json-template.golden | 1 - ...inspect-with-format.simple-template.golden | 1 - ...format.multiple-secrets-with-labels.golden | 26 - ...nspect-without-format.single-secret.golden | 12 - .../secret-list-with-config-format.golden | 2 - .../testdata/secret-list-with-filter.golden | 3 - .../testdata/secret-list-with-format.golden | 2 - .../secret-list-with-quiet-option.golden | 2 - .../secret/testdata/secret-list.golden | 3 - cli/command/service/cmd.go | 30 - cli/command/service/create.go | 118 -- cli/command/service/helpers.go | 39 - cli/command/service/inspect.go | 94 -- cli/command/service/inspect_test.go | 140 -- cli/command/service/list.go | 129 -- cli/command/service/logs.go | 301 ----- cli/command/service/opts.go | 912 ------------- cli/command/service/opts_test.go | 108 -- cli/command/service/parse.go | 59 - cli/command/service/progress/progress.go | 409 ------ cli/command/service/ps.go | 123 -- cli/command/service/remove.go | 48 - cli/command/service/scale.go | 97 -- cli/command/service/trust.go | 87 -- cli/command/service/update.go | 1018 --------------- cli/command/service/update_test.go | 496 ------- cli/command/stack/client_test.go | 153 --- cli/command/stack/cmd.go | 35 - cli/command/stack/common.go | 64 - cli/command/stack/deploy.go | 97 -- cli/command/stack/deploy_bundlefile.go | 91 -- cli/command/stack/deploy_composefile.go | 316 ----- cli/command/stack/deploy_composefile_test.go | 28 - cli/command/stack/deploy_test.go | 27 - cli/command/stack/list.go | 95 -- cli/command/stack/opts.go | 51 - cli/command/stack/ps.go | 76 -- cli/command/stack/remove.go | 121 -- cli/command/stack/remove_test.go | 107 -- cli/command/stack/services.go | 97 -- cli/command/stream.go | 34 - cli/command/swarm/client_test.go | 84 -- cli/command/swarm/cmd.go | 29 - cli/command/swarm/init.go | 99 -- cli/command/swarm/init_test.go | 131 -- cli/command/swarm/join.go | 88 -- cli/command/swarm/join_test.go | 103 -- cli/command/swarm/join_token.go | 119 -- cli/command/swarm/join_token_test.go | 217 ---- cli/command/swarm/leave.go | 44 - cli/command/swarm/leave_test.go | 53 - cli/command/swarm/opts.go | 224 ---- cli/command/swarm/opts_test.go | 110 -- .../swarm/testdata/init-init-autolock.golden | 11 - cli/command/swarm/testdata/init-init.golden | 4 - .../testdata/jointoken-manager-quiet.golden | 1 - .../testdata/jointoken-manager-rotate.golden | 5 - .../swarm/testdata/jointoken-manager.golden | 3 - .../testdata/jointoken-worker-quiet.golden | 1 - .../swarm/testdata/jointoken-worker.golden | 3 - .../unlockkeys-unlock-key-quiet.golden | 1 - .../unlockkeys-unlock-key-rotate-quiet.golden | 1 - .../unlockkeys-unlock-key-rotate.golden | 9 - .../testdata/unlockkeys-unlock-key.golden | 7 - .../testdata/update-all-flags-quiet.golden | 1 - .../update-autolock-unlock-key.golden | 8 - .../swarm/testdata/update-noargs.golden | 13 - cli/command/swarm/unlock.go | 78 -- cli/command/swarm/unlock_key.go | 86 -- cli/command/swarm/unlock_key_test.go | 177 --- cli/command/swarm/unlock_test.go | 103 -- cli/command/swarm/update.go | 72 -- cli/command/swarm/update_test.go | 184 --- cli/command/system/cmd.go | 26 - cli/command/system/df.go | 68 - cli/command/system/events.go | 140 -- cli/command/system/info.go | 369 ------ cli/command/system/inspect.go | 216 ---- cli/command/system/prune.go | 96 -- cli/command/system/version.go | 131 -- cli/command/task/print.go | 84 -- cli/command/trust.go | 43 - cli/command/utils.go | 119 -- cli/command/volume/client_test.go | 53 - cli/command/volume/cmd.go | 26 - cli/command/volume/create.go | 70 - cli/command/volume/create_test.go | 143 --- cli/command/volume/inspect.go | 45 - cli/command/volume/inspect_test.go | 152 --- cli/command/volume/list.go | 73 -- cli/command/volume/list_test.go | 125 -- cli/command/volume/prune.go | 78 -- cli/command/volume/prune_test.go | 135 -- cli/command/volume/remove.go | 69 - cli/command/volume/remove_test.go | 48 - ...e-inspect-with-format.json-template.golden | 1 - ...inspect-with-format.simple-template.golden | 1 - ...-format.multiple-volume-with-labels.golden | 22 - ...nspect-without-format.single-volume.golden | 10 - .../volume-list-with-config-format.golden | 3 - .../testdata/volume-list-with-format.golden | 3 - .../volume-list-without-format.golden | 4 - .../volume/testdata/volume-prune-no.golden | 2 - .../volume/testdata/volume-prune-yes.golden | 7 - .../volume-prune.deletedVolumes.golden | 6 - .../volume/testdata/volume-prune.empty.golden | 1 - cli/compose/convert/compose.go | 118 -- cli/compose/convert/compose_test.go | 135 -- cli/compose/convert/service.go | 464 ------- cli/compose/convert/service_test.go | 318 ----- cli/compose/convert/volume.go | 87 -- cli/compose/convert/volume_test.go | 192 --- cli/compose/interpolation/interpolation.go | 92 -- .../interpolation/interpolation_test.go | 57 - cli/compose/loader/example1.env | 8 - cli/compose/loader/example2.env | 4 - cli/compose/loader/full-example.yml | 290 ----- cli/compose/loader/loader.go | 702 ---------- cli/compose/loader/loader_test.go | 1135 ----------------- cli/compose/loader/volume.go | 121 -- cli/compose/loader/volume_test.go | 149 --- cli/compose/schema/bindata.go | 306 ----- .../schema/data/config_schema_v3.0.json | 383 ------ .../schema/data/config_schema_v3.1.json | 428 ------- .../schema/data/config_schema_v3.2.json | 474 ------- cli/compose/schema/schema.go | 168 --- cli/compose/schema/schema_test.go | 52 - cli/compose/template/template.go | 100 -- cli/compose/template/template_test.go | 83 -- cli/compose/types/types.go | 307 ----- cli/config/config.go | 120 -- cli/config/config_test.go | 621 --------- cli/config/configfile/file.go | 189 --- cli/config/configfile/file_test.go | 27 - cli/config/credentials/credentials.go | 17 - cli/config/credentials/default_store.go | 22 - .../credentials/default_store_darwin.go | 3 - cli/config/credentials/default_store_linux.go | 3 - .../credentials/default_store_unsupported.go | 5 - .../credentials/default_store_windows.go | 3 - cli/config/credentials/file_store.go | 53 - cli/config/credentials/file_store_test.go | 139 -- cli/config/credentials/native_store.go | 144 --- cli/config/credentials/native_store_test.go | 356 ------ cli/flags/common.go | 4 +- cli/flags/common_test.go | 4 +- cli/internal/test/builders/doc.go | 3 - cli/internal/test/builders/node.go | 127 -- cli/internal/test/builders/secret.go | 61 - cli/internal/test/builders/service.go | 32 - cli/internal/test/builders/swarm.go | 39 - cli/internal/test/builders/task.go | 111 -- cli/internal/test/builders/volume.go | 43 - cli/internal/test/cli.go | 81 -- cli/internal/test/doc.go | 5 - cli/internal/test/store.go | 74 -- cli/trust/trust.go | 232 ---- cmd/docker/daemon_none.go | 29 - cmd/docker/daemon_none_test.go | 17 - cmd/docker/daemon_unit_test.go | 30 - cmd/docker/daemon_unix.go | 79 -- cmd/docker/docker.go | 310 ----- cmd/docker/docker_test.go | 32 - cmd/docker/docker_windows.go | 18 - cmd/dockerd/daemon.go | 4 +- docs/yaml/Dockerfile | 4 - docs/yaml/generate.go | 86 -- docs/yaml/yaml.go | 212 --- hack/dockerfile/binaries-commits | 4 + hack/dockerfile/install-binaries.sh | 20 +- hack/make.ps1 | 12 +- hack/make.sh | 1 - hack/make/.build-deb/rules | 2 - hack/make/.build-rpm/docker-engine.spec | 2 - hack/make/.integration-daemon-setup | 2 +- hack/make/.integration-daemon-start | 19 +- hack/make/.integration-test-helpers | 2 + hack/make/binary | 5 - hack/make/binary-client | 12 - hack/make/cross | 17 +- hack/make/dynbinary | 5 - hack/make/dynbinary-client | 12 - hack/make/install-binary-client | 10 - hack/make/tgz | 92 +- hooks/post_build | 19 - integration-cli/check_test.go | 6 +- integration-cli/docker_cli_config_test.go | 4 +- integration-cli/docker_cli_push_test.go | 23 +- integration-cli/environment/environment.go | 20 +- integration-cli/trust_server_test.go | 4 +- 401 files changed, 139 insertions(+), 40750 deletions(-) create mode 100644 cli/cli.go delete mode 100644 cli/command/bundlefile/bundlefile.go delete mode 100644 cli/command/bundlefile/bundlefile_test.go delete mode 100644 cli/command/checkpoint/cmd.go delete mode 100644 cli/command/checkpoint/create.go delete mode 100644 cli/command/checkpoint/list.go delete mode 100644 cli/command/checkpoint/remove.go delete mode 100644 cli/command/cli.go delete mode 100644 cli/command/commands/commands.go delete mode 100644 cli/command/container/attach.go delete mode 100644 cli/command/container/cmd.go delete mode 100644 cli/command/container/commit.go delete mode 100644 cli/command/container/cp.go delete mode 100644 cli/command/container/create.go delete mode 100644 cli/command/container/diff.go delete mode 100644 cli/command/container/exec.go delete mode 100644 cli/command/container/exec_test.go delete mode 100644 cli/command/container/export.go delete mode 100644 cli/command/container/hijack.go delete mode 100644 cli/command/container/inspect.go delete mode 100644 cli/command/container/kill.go delete mode 100644 cli/command/container/list.go delete mode 100644 cli/command/container/logs.go delete mode 100644 cli/command/container/opts.go delete mode 100644 cli/command/container/opts_test.go delete mode 100644 cli/command/container/pause.go delete mode 100644 cli/command/container/port.go delete mode 100644 cli/command/container/prune.go delete mode 100644 cli/command/container/ps_test.go delete mode 100644 cli/command/container/rename.go delete mode 100644 cli/command/container/restart.go delete mode 100644 cli/command/container/rm.go delete mode 100644 cli/command/container/run.go delete mode 100644 cli/command/container/start.go delete mode 100644 cli/command/container/stats.go delete mode 100644 cli/command/container/stats_helpers.go delete mode 100644 cli/command/container/stats_unit_test.go delete mode 100644 cli/command/container/stop.go delete mode 100755 cli/command/container/testdata/utf16.env delete mode 100755 cli/command/container/testdata/utf16be.env delete mode 100755 cli/command/container/testdata/utf8.env delete mode 100644 cli/command/container/testdata/valid.env delete mode 100644 cli/command/container/testdata/valid.label delete mode 100644 cli/command/container/top.go delete mode 100644 cli/command/container/tty.go delete mode 100644 cli/command/container/unpause.go delete mode 100644 cli/command/container/update.go delete mode 100644 cli/command/container/utils.go delete mode 100644 cli/command/container/wait.go delete mode 100644 cli/command/events_utils.go delete mode 100644 cli/command/formatter/checkpoint.go delete mode 100644 cli/command/formatter/checkpoint_test.go delete mode 100644 cli/command/formatter/container.go delete mode 100644 cli/command/formatter/container_test.go delete mode 100644 cli/command/formatter/custom.go delete mode 100644 cli/command/formatter/custom_test.go delete mode 100644 cli/command/formatter/diff.go delete mode 100644 cli/command/formatter/diff_test.go delete mode 100644 cli/command/formatter/disk_usage.go delete mode 100644 cli/command/formatter/disk_usage_test.go delete mode 100644 cli/command/formatter/formatter.go delete mode 100644 cli/command/formatter/history.go delete mode 100644 cli/command/formatter/history_test.go delete mode 100644 cli/command/formatter/image.go delete mode 100644 cli/command/formatter/image_test.go delete mode 100644 cli/command/formatter/network.go delete mode 100644 cli/command/formatter/network_test.go delete mode 100644 cli/command/formatter/node.go delete mode 100644 cli/command/formatter/node_test.go delete mode 100644 cli/command/formatter/plugin.go delete mode 100644 cli/command/formatter/plugin_test.go delete mode 100644 cli/command/formatter/reflect.go delete mode 100644 cli/command/formatter/reflect_test.go delete mode 100644 cli/command/formatter/secret.go delete mode 100644 cli/command/formatter/secret_test.go delete mode 100644 cli/command/formatter/service.go delete mode 100644 cli/command/formatter/service_test.go delete mode 100644 cli/command/formatter/stack.go delete mode 100644 cli/command/formatter/stack_test.go delete mode 100644 cli/command/formatter/stats.go delete mode 100644 cli/command/formatter/stats_test.go delete mode 100644 cli/command/formatter/task.go delete mode 100644 cli/command/formatter/task_test.go delete mode 100644 cli/command/formatter/volume.go delete mode 100644 cli/command/formatter/volume_test.go delete mode 100644 cli/command/idresolver/client_test.go delete mode 100644 cli/command/idresolver/idresolver.go delete mode 100644 cli/command/idresolver/idresolver_test.go delete mode 100644 cli/command/image/build.go delete mode 100644 cli/command/image/build/context.go delete mode 100644 cli/command/image/build/context_test.go delete mode 100644 cli/command/image/build/context_unix.go delete mode 100644 cli/command/image/build/context_windows.go delete mode 100644 cli/command/image/client_test.go delete mode 100644 cli/command/image/cmd.go delete mode 100644 cli/command/image/history.go delete mode 100644 cli/command/image/history_test.go delete mode 100644 cli/command/image/import.go delete mode 100644 cli/command/image/import_test.go delete mode 100644 cli/command/image/inspect.go delete mode 100644 cli/command/image/inspect_test.go delete mode 100644 cli/command/image/list.go delete mode 100644 cli/command/image/list_test.go delete mode 100644 cli/command/image/load.go delete mode 100644 cli/command/image/load_test.go delete mode 100644 cli/command/image/prune.go delete mode 100644 cli/command/image/prune_test.go delete mode 100644 cli/command/image/pull.go delete mode 100644 cli/command/image/pull_test.go delete mode 100644 cli/command/image/push.go delete mode 100644 cli/command/image/push_test.go delete mode 100644 cli/command/image/remove.go delete mode 100644 cli/command/image/remove_test.go delete mode 100644 cli/command/image/save.go delete mode 100644 cli/command/image/save_test.go delete mode 100644 cli/command/image/tag.go delete mode 100644 cli/command/image/tag_test.go delete mode 100644 cli/command/image/testdata/history-command-success.quiet-no-trunc.golden delete mode 100644 cli/command/image/testdata/history-command-success.quiet.golden delete mode 100644 cli/command/image/testdata/history-command-success.simple.golden delete mode 100644 cli/command/image/testdata/import-command-success.input.txt delete mode 100644 cli/command/image/testdata/inspect-command-success.format.golden delete mode 100644 cli/command/image/testdata/inspect-command-success.simple-many.golden delete mode 100644 cli/command/image/testdata/inspect-command-success.simple.golden delete mode 100644 cli/command/image/testdata/list-command-success.filters.golden delete mode 100644 cli/command/image/testdata/list-command-success.format.golden delete mode 100644 cli/command/image/testdata/list-command-success.match-name.golden delete mode 100644 cli/command/image/testdata/list-command-success.quiet-format.golden delete mode 100644 cli/command/image/testdata/list-command-success.simple.golden delete mode 100644 cli/command/image/testdata/load-command-success.input-file.golden delete mode 100644 cli/command/image/testdata/load-command-success.input.txt delete mode 100644 cli/command/image/testdata/load-command-success.json.golden delete mode 100644 cli/command/image/testdata/load-command-success.simple.golden delete mode 100644 cli/command/image/testdata/prune-command-success.all.golden delete mode 100644 cli/command/image/testdata/prune-command-success.force-deleted.golden delete mode 100644 cli/command/image/testdata/prune-command-success.force-untagged.golden delete mode 100644 cli/command/image/testdata/pull-command-success.simple-no-tag.golden delete mode 100644 cli/command/image/testdata/pull-command-success.simple.golden delete mode 100644 cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden delete mode 100644 cli/command/image/testdata/remove-command-success.Image Deleted.golden delete mode 100644 cli/command/image/testdata/remove-command-success.Image Untagged.golden delete mode 100644 cli/command/image/trust.go delete mode 100644 cli/command/image/trust_test.go delete mode 100644 cli/command/in.go delete mode 100644 cli/command/inspect/inspector.go delete mode 100644 cli/command/inspect/inspector_test.go delete mode 100644 cli/command/network/cmd.go delete mode 100644 cli/command/network/connect.go delete mode 100644 cli/command/network/create.go delete mode 100644 cli/command/network/disconnect.go delete mode 100644 cli/command/network/inspect.go delete mode 100644 cli/command/network/list.go delete mode 100644 cli/command/network/prune.go delete mode 100644 cli/command/network/remove.go delete mode 100644 cli/command/node/client_test.go delete mode 100644 cli/command/node/cmd.go delete mode 100644 cli/command/node/demote.go delete mode 100644 cli/command/node/demote_test.go delete mode 100644 cli/command/node/inspect.go delete mode 100644 cli/command/node/inspect_test.go delete mode 100644 cli/command/node/list.go delete mode 100644 cli/command/node/list_test.go delete mode 100644 cli/command/node/opts.go delete mode 100644 cli/command/node/promote.go delete mode 100644 cli/command/node/promote_test.go delete mode 100644 cli/command/node/ps.go delete mode 100644 cli/command/node/ps_test.go delete mode 100644 cli/command/node/remove.go delete mode 100644 cli/command/node/remove_test.go delete mode 100644 cli/command/node/testdata/node-inspect-pretty.manager-leader.golden delete mode 100644 cli/command/node/testdata/node-inspect-pretty.manager.golden delete mode 100644 cli/command/node/testdata/node-inspect-pretty.simple.golden delete mode 100644 cli/command/node/testdata/node-ps.simple.golden delete mode 100644 cli/command/node/testdata/node-ps.with-errors.golden delete mode 100644 cli/command/node/update.go delete mode 100644 cli/command/node/update_test.go delete mode 100644 cli/command/out.go delete mode 100644 cli/command/plugin/cmd.go delete mode 100644 cli/command/plugin/create.go delete mode 100644 cli/command/plugin/disable.go delete mode 100644 cli/command/plugin/enable.go delete mode 100644 cli/command/plugin/inspect.go delete mode 100644 cli/command/plugin/install.go delete mode 100644 cli/command/plugin/list.go delete mode 100644 cli/command/plugin/push.go delete mode 100644 cli/command/plugin/remove.go delete mode 100644 cli/command/plugin/set.go delete mode 100644 cli/command/plugin/upgrade.go delete mode 100644 cli/command/prune/prune.go delete mode 100644 cli/command/registry.go delete mode 100644 cli/command/registry/login.go delete mode 100644 cli/command/registry/logout.go delete mode 100644 cli/command/registry/search.go delete mode 100644 cli/command/secret/client_test.go delete mode 100644 cli/command/secret/cmd.go delete mode 100644 cli/command/secret/create.go delete mode 100644 cli/command/secret/create_test.go delete mode 100644 cli/command/secret/inspect.go delete mode 100644 cli/command/secret/inspect_test.go delete mode 100644 cli/command/secret/ls.go delete mode 100644 cli/command/secret/ls_test.go delete mode 100644 cli/command/secret/remove.go delete mode 100644 cli/command/secret/remove_test.go delete mode 100644 cli/command/secret/testdata/secret-create-with-name.golden delete mode 100644 cli/command/secret/testdata/secret-inspect-with-format.json-template.golden delete mode 100644 cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden delete mode 100644 cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden delete mode 100644 cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden delete mode 100644 cli/command/secret/testdata/secret-list-with-config-format.golden delete mode 100644 cli/command/secret/testdata/secret-list-with-filter.golden delete mode 100644 cli/command/secret/testdata/secret-list-with-format.golden delete mode 100644 cli/command/secret/testdata/secret-list-with-quiet-option.golden delete mode 100644 cli/command/secret/testdata/secret-list.golden delete mode 100644 cli/command/service/cmd.go delete mode 100644 cli/command/service/create.go delete mode 100644 cli/command/service/helpers.go delete mode 100644 cli/command/service/inspect.go delete mode 100644 cli/command/service/inspect_test.go delete mode 100644 cli/command/service/list.go delete mode 100644 cli/command/service/logs.go delete mode 100644 cli/command/service/opts.go delete mode 100644 cli/command/service/opts_test.go delete mode 100644 cli/command/service/parse.go delete mode 100644 cli/command/service/progress/progress.go delete mode 100644 cli/command/service/ps.go delete mode 100644 cli/command/service/remove.go delete mode 100644 cli/command/service/scale.go delete mode 100644 cli/command/service/trust.go delete mode 100644 cli/command/service/update.go delete mode 100644 cli/command/service/update_test.go delete mode 100644 cli/command/stack/client_test.go delete mode 100644 cli/command/stack/cmd.go delete mode 100644 cli/command/stack/common.go delete mode 100644 cli/command/stack/deploy.go delete mode 100644 cli/command/stack/deploy_bundlefile.go delete mode 100644 cli/command/stack/deploy_composefile.go delete mode 100644 cli/command/stack/deploy_composefile_test.go delete mode 100644 cli/command/stack/deploy_test.go delete mode 100644 cli/command/stack/list.go delete mode 100644 cli/command/stack/opts.go delete mode 100644 cli/command/stack/ps.go delete mode 100644 cli/command/stack/remove.go delete mode 100644 cli/command/stack/remove_test.go delete mode 100644 cli/command/stack/services.go delete mode 100644 cli/command/stream.go delete mode 100644 cli/command/swarm/client_test.go delete mode 100644 cli/command/swarm/cmd.go delete mode 100644 cli/command/swarm/init.go delete mode 100644 cli/command/swarm/init_test.go delete mode 100644 cli/command/swarm/join.go delete mode 100644 cli/command/swarm/join_test.go delete mode 100644 cli/command/swarm/join_token.go delete mode 100644 cli/command/swarm/join_token_test.go delete mode 100644 cli/command/swarm/leave.go delete mode 100644 cli/command/swarm/leave_test.go delete mode 100644 cli/command/swarm/opts.go delete mode 100644 cli/command/swarm/opts_test.go delete mode 100644 cli/command/swarm/testdata/init-init-autolock.golden delete mode 100644 cli/command/swarm/testdata/init-init.golden delete mode 100644 cli/command/swarm/testdata/jointoken-manager-quiet.golden delete mode 100644 cli/command/swarm/testdata/jointoken-manager-rotate.golden delete mode 100644 cli/command/swarm/testdata/jointoken-manager.golden delete mode 100644 cli/command/swarm/testdata/jointoken-worker-quiet.golden delete mode 100644 cli/command/swarm/testdata/jointoken-worker.golden delete mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden delete mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden delete mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden delete mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key.golden delete mode 100644 cli/command/swarm/testdata/update-all-flags-quiet.golden delete mode 100644 cli/command/swarm/testdata/update-autolock-unlock-key.golden delete mode 100644 cli/command/swarm/testdata/update-noargs.golden delete mode 100644 cli/command/swarm/unlock.go delete mode 100644 cli/command/swarm/unlock_key.go delete mode 100644 cli/command/swarm/unlock_key_test.go delete mode 100644 cli/command/swarm/unlock_test.go delete mode 100644 cli/command/swarm/update.go delete mode 100644 cli/command/swarm/update_test.go delete mode 100644 cli/command/system/cmd.go delete mode 100644 cli/command/system/df.go delete mode 100644 cli/command/system/events.go delete mode 100644 cli/command/system/info.go delete mode 100644 cli/command/system/inspect.go delete mode 100644 cli/command/system/prune.go delete mode 100644 cli/command/system/version.go delete mode 100644 cli/command/task/print.go delete mode 100644 cli/command/trust.go delete mode 100644 cli/command/utils.go delete mode 100644 cli/command/volume/client_test.go delete mode 100644 cli/command/volume/cmd.go delete mode 100644 cli/command/volume/create.go delete mode 100644 cli/command/volume/create_test.go delete mode 100644 cli/command/volume/inspect.go delete mode 100644 cli/command/volume/inspect_test.go delete mode 100644 cli/command/volume/list.go delete mode 100644 cli/command/volume/list_test.go delete mode 100644 cli/command/volume/prune.go delete mode 100644 cli/command/volume/prune_test.go delete mode 100644 cli/command/volume/remove.go delete mode 100644 cli/command/volume/remove_test.go delete mode 100644 cli/command/volume/testdata/volume-inspect-with-format.json-template.golden delete mode 100644 cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden delete mode 100644 cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden delete mode 100644 cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden delete mode 100644 cli/command/volume/testdata/volume-list-with-config-format.golden delete mode 100644 cli/command/volume/testdata/volume-list-with-format.golden delete mode 100644 cli/command/volume/testdata/volume-list-without-format.golden delete mode 100644 cli/command/volume/testdata/volume-prune-no.golden delete mode 100644 cli/command/volume/testdata/volume-prune-yes.golden delete mode 100644 cli/command/volume/testdata/volume-prune.deletedVolumes.golden delete mode 100644 cli/command/volume/testdata/volume-prune.empty.golden delete mode 100644 cli/compose/convert/compose.go delete mode 100644 cli/compose/convert/compose_test.go delete mode 100644 cli/compose/convert/service.go delete mode 100644 cli/compose/convert/service_test.go delete mode 100644 cli/compose/convert/volume.go delete mode 100644 cli/compose/convert/volume_test.go delete mode 100644 cli/compose/interpolation/interpolation.go delete mode 100644 cli/compose/interpolation/interpolation_test.go delete mode 100644 cli/compose/loader/example1.env delete mode 100644 cli/compose/loader/example2.env delete mode 100644 cli/compose/loader/full-example.yml delete mode 100644 cli/compose/loader/loader.go delete mode 100644 cli/compose/loader/loader_test.go delete mode 100644 cli/compose/loader/volume.go delete mode 100644 cli/compose/loader/volume_test.go delete mode 100644 cli/compose/schema/bindata.go delete mode 100644 cli/compose/schema/data/config_schema_v3.0.json delete mode 100644 cli/compose/schema/data/config_schema_v3.1.json delete mode 100644 cli/compose/schema/data/config_schema_v3.2.json delete mode 100644 cli/compose/schema/schema.go delete mode 100644 cli/compose/schema/schema_test.go delete mode 100644 cli/compose/template/template.go delete mode 100644 cli/compose/template/template_test.go delete mode 100644 cli/compose/types/types.go delete mode 100644 cli/config/config.go delete mode 100644 cli/config/config_test.go delete mode 100644 cli/config/configfile/file.go delete mode 100644 cli/config/configfile/file_test.go delete mode 100644 cli/config/credentials/credentials.go delete mode 100644 cli/config/credentials/default_store.go delete mode 100644 cli/config/credentials/default_store_darwin.go delete mode 100644 cli/config/credentials/default_store_linux.go delete mode 100644 cli/config/credentials/default_store_unsupported.go delete mode 100644 cli/config/credentials/default_store_windows.go delete mode 100644 cli/config/credentials/file_store.go delete mode 100644 cli/config/credentials/file_store_test.go delete mode 100644 cli/config/credentials/native_store.go delete mode 100644 cli/config/credentials/native_store_test.go delete mode 100644 cli/internal/test/builders/doc.go delete mode 100644 cli/internal/test/builders/node.go delete mode 100644 cli/internal/test/builders/secret.go delete mode 100644 cli/internal/test/builders/service.go delete mode 100644 cli/internal/test/builders/swarm.go delete mode 100644 cli/internal/test/builders/task.go delete mode 100644 cli/internal/test/builders/volume.go delete mode 100644 cli/internal/test/cli.go delete mode 100644 cli/internal/test/doc.go delete mode 100644 cli/internal/test/store.go delete mode 100644 cli/trust/trust.go delete mode 100644 cmd/docker/daemon_none.go delete mode 100644 cmd/docker/daemon_none_test.go delete mode 100644 cmd/docker/daemon_unit_test.go delete mode 100644 cmd/docker/daemon_unix.go delete mode 100644 cmd/docker/docker.go delete mode 100644 cmd/docker/docker_test.go delete mode 100644 cmd/docker/docker_windows.go delete mode 100644 docs/yaml/Dockerfile delete mode 100644 docs/yaml/generate.go delete mode 100644 docs/yaml/yaml.go delete mode 100644 hack/make/binary-client delete mode 100644 hack/make/dynbinary-client delete mode 100644 hack/make/install-binary-client delete mode 100755 hooks/post_build diff --git a/Dockerfile b/Dockerfile index 27608c4f5f..1f25c92255 100644 --- a/Dockerfile +++ b/Dockerfile @@ -241,11 +241,12 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ hello-world:latest@sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7 # See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list) -# Install tomlv, vndr, runc, containerd, tini, docker-proxy +# Install tomlv, vndr, runc, containerd, tini, docker-proxy dockercli # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 index 02782eeeb6..e70853e95e 100644 --- a/Dockerfile.aarch64 +++ b/Dockerfile.aarch64 @@ -192,7 +192,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b972c1b087..c12be6d488 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -173,7 +173,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.ppc64le b/Dockerfile.ppc64le index d4fd8fd491..f240cc1f82 100644 --- a/Dockerfile.ppc64le +++ b/Dockerfile.ppc64le @@ -179,7 +179,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.s390x b/Dockerfile.s390x index 4d9af91806..f82ebb4f92 100644 --- a/Dockerfile.s390x +++ b/Dockerfile.s390x @@ -172,7 +172,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.simple b/Dockerfile.simple index 248f88de3e..512828e5ed 100644 --- a/Dockerfile.simple +++ b/Dockerfile.simple @@ -64,7 +64,8 @@ ENV CGO_LDFLAGS -L/lib # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh runc containerd tini proxy +RUN /tmp/install-binaries.sh runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH ENV AUTO_GOPATH 1 WORKDIR /usr/src/docker diff --git a/Makefile b/Makefile index 9fa993b635..977091ba0d 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKER_INCREMENTAL_BINARY := $(if $(DOCKER_INCREMENTAL_BINARY),$(DOCKER_INCREMEN export DOCKER_INCREMENTAL_BINARY # get OS/Arch of docker engine -DOCKER_OSARCH := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKER_ENGINE_OSARCH:-$$DOCKER_CLIENT_OSARCH}') +DOCKER_OSARCH := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKER_ENGINE_OSARCH}') DOCKERFILE := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKERFILE}') DOCKER_GITCOMMIT := $(shell git rev-parse --short HEAD || echo unsupported) @@ -24,6 +24,7 @@ DOCKER_ENVS := \ -e DOCKER_BUILD_ARGS \ -e DOCKER_BUILD_GOGC \ -e DOCKER_BUILD_PKGS \ + -e DOCKER_CLI_PATH \ -e DOCKER_DEBUG \ -e DOCKER_EXPERIMENTAL \ -e DOCKER_GITCOMMIT \ @@ -63,7 +64,8 @@ PKGCACHE_MAP := gopath:/go/pkg goroot-linux_amd64:/usr/local/go/pkg/linux_amd64 PKGCACHE_VOLROOT := dockerdev-go-pkg-cache PKGCACHE_VOL := $(if $(PKGCACHE_DIR),$(CURDIR)/$(PKGCACHE_DIR)/,$(PKGCACHE_VOLROOT)-) DOCKER_MOUNT_PKGCACHE := $(if $(DOCKER_INCREMENTAL_BINARY),$(shell echo $(PKGCACHE_MAP) | sed -E 's@([^ ]*)@-v "$(PKGCACHE_VOL)\1"@g'),) -DOCKER_MOUNT := $(DOCKER_MOUNT) $(DOCKER_MOUNT_PKGCACHE) +DOCKER_MOUNT_CLI := $(if $(DOCKER_CLI_PATH),-v $(shell dirname $(DOCKER_CLI_PATH)):/usr/local/cli,) +DOCKER_MOUNT := $(DOCKER_MOUNT) $(DOCKER_MOUNT_PKGCACHE) $(DOCKER_MOUNT_CLI) GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") @@ -79,6 +81,11 @@ SWAGGER_DOCS_PORT ?= 9000 INTEGRATION_CLI_MASTER_IMAGE := $(if $(INTEGRATION_CLI_MASTER_IMAGE), $(INTEGRATION_CLI_MASTER_IMAGE), integration-cli-master) INTEGRATION_CLI_WORKER_IMAGE := $(if $(INTEGRATION_CLI_WORKER_IMAGE), $(INTEGRATION_CLI_WORKER_IMAGE), integration-cli-worker) +define \n + + +endef + # if this session isn't interactive, then we don't want to allocate a # TTY, which would fail, but if it is interactive, we do want to attach # so that the user can send e.g. ^C through. @@ -98,6 +105,7 @@ binary: build ## build the linux binaries $(DOCKER_RUN_DOCKER) hack/make.sh binary build: bundles init-go-pkg-cache + $(warning The docker client CLI has moved to github.com/docker/cli. By default, it is built from the git sha specified in hack/dockerfile/binaries-commits. For a dev-test cycle involving the CLI, run:${\n} DOCKER_CLI_PATH=/host/path/to/cli/binary make shell ${\n} then change the cli and compile into a binary at the same location.${\n}) docker build ${BUILD_APT_MIRROR} ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" . bundles: diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000000..4281667192 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,25 @@ +package cli + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/homedir" +) + +var ( + configDir = os.Getenv("DOCKER_CONFIG") + configFileDir = ".docker" +) + +// ConfigurationDir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable. +// TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves +func ConfigurationDir() string { + return configDir +} + +func init() { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), configFileDir) + } +} diff --git a/cli/command/bundlefile/bundlefile.go b/cli/command/bundlefile/bundlefile.go deleted file mode 100644 index 07e2c8b081..0000000000 --- a/cli/command/bundlefile/bundlefile.go +++ /dev/null @@ -1,70 +0,0 @@ -package bundlefile - -import ( - "encoding/json" - "io" - - "github.com/pkg/errors" -) - -// Bundlefile stores the contents of a bundlefile -type Bundlefile struct { - Version string - Services map[string]Service -} - -// Service is a service from a bundlefile -type Service struct { - Image string - Command []string `json:",omitempty"` - Args []string `json:",omitempty"` - Env []string `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Ports []Port `json:",omitempty"` - WorkingDir *string `json:",omitempty"` - User *string `json:",omitempty"` - Networks []string `json:",omitempty"` -} - -// Port is a port as defined in a bundlefile -type Port struct { - Protocol string - Port uint32 -} - -// LoadFile loads a bundlefile from a path to the file -func LoadFile(reader io.Reader) (*Bundlefile, error) { - bundlefile := &Bundlefile{} - - decoder := json.NewDecoder(reader) - if err := decoder.Decode(bundlefile); err != nil { - switch jsonErr := err.(type) { - case *json.SyntaxError: - return nil, errors.Errorf( - "JSON syntax error at byte %v: %s", - jsonErr.Offset, - jsonErr.Error()) - case *json.UnmarshalTypeError: - return nil, errors.Errorf( - "Unexpected type at byte %v. Expected %s but received %s.", - jsonErr.Offset, - jsonErr.Type, - jsonErr.Value) - } - return nil, err - } - - return bundlefile, nil -} - -// Print writes the contents of the bundlefile to the output writer -// as human readable json -func Print(out io.Writer, bundle *Bundlefile) error { - bytes, err := json.MarshalIndent(*bundle, "", " ") - if err != nil { - return err - } - - _, err = out.Write(bytes) - return err -} diff --git a/cli/command/bundlefile/bundlefile_test.go b/cli/command/bundlefile/bundlefile_test.go deleted file mode 100644 index bd059c4dca..0000000000 --- a/cli/command/bundlefile/bundlefile_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package bundlefile - -import ( - "bytes" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadFileV01Success(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "redis": { - "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", - "Networks": ["default"] - }, - "web": { - "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", - "Networks": ["default"], - "User": "web" - } - } - }`) - - bundle, err := LoadFile(reader) - assert.NoError(t, err) - assert.Equal(t, "0.1", bundle.Version) - assert.Len(t, bundle.Services, 2) -} - -func TestLoadFileSyntaxError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": unquoted string - }`) - - _, err := LoadFile(reader) - assert.EqualError(t, err, "JSON syntax error at byte 37: invalid character 'u' looking for beginning of value") -} - -func TestLoadFileTypeError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "web": { - "Image": "redis", - "Networks": "none" - } - } - }`) - - _, err := LoadFile(reader) - assert.EqualError(t, err, "Unexpected type at byte 94. Expected []string but received string.") -} - -func TestPrint(t *testing.T) { - var buffer bytes.Buffer - bundle := &Bundlefile{ - Version: "0.1", - Services: map[string]Service{ - "web": { - Image: "image", - Command: []string{"echo", "something"}, - }, - }, - } - assert.NoError(t, Print(&buffer, bundle)) - output := buffer.String() - assert.Contains(t, output, "\"Image\": \"image\"") - assert.Contains(t, output, - `"Command": [ - "echo", - "something" - ]`) -} diff --git a/cli/command/checkpoint/cmd.go b/cli/command/checkpoint/cmd.go deleted file mode 100644 index d5705a4dad..0000000000 --- a/cli/command/checkpoint/cmd.go +++ /dev/null @@ -1,24 +0,0 @@ -package checkpoint - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) -func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "checkpoint", - Short: "Manage checkpoints", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"experimental": "", "version": "1.25"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/checkpoint/create.go b/cli/command/checkpoint/create.go deleted file mode 100644 index 473a941733..0000000000 --- a/cli/command/checkpoint/create.go +++ /dev/null @@ -1,58 +0,0 @@ -package checkpoint - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type createOptions struct { - container string - checkpoint string - checkpointDir string - leaveRunning bool -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts createOptions - - cmd := &cobra.Command{ - Use: "create [OPTIONS] CONTAINER CHECKPOINT", - Short: "Create a checkpoint from a running container", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - opts.checkpoint = args[1] - return runCreate(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { - client := dockerCli.Client() - - checkpointOpts := types.CheckpointCreateOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - Exit: !opts.leaveRunning, - } - - err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts) - if err != nil { - return err - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint) - return nil -} diff --git a/cli/command/checkpoint/list.go b/cli/command/checkpoint/list.go deleted file mode 100644 index 20e7d6d73a..0000000000 --- a/cli/command/checkpoint/list.go +++ /dev/null @@ -1,54 +0,0 @@ -package checkpoint - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" -) - -type listOptions struct { - checkpointDir string -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions - - cmd := &cobra.Command{ - Use: "ls [OPTIONS] CONTAINER", - Aliases: []string{"list"}, - Short: "List checkpoints for a container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, args[0], opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd - -} - -func runList(dockerCli *command.DockerCli, container string, opts listOptions) error { - client := dockerCli.Client() - - listOpts := types.CheckpointListOptions{ - CheckpointDir: opts.checkpointDir, - } - - checkpoints, err := client.CheckpointList(context.Background(), container, listOpts) - if err != nil { - return err - } - - cpCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewCheckpointFormat(formatter.TableFormatKey), - } - return formatter.CheckpointWrite(cpCtx, checkpoints) -} diff --git a/cli/command/checkpoint/remove.go b/cli/command/checkpoint/remove.go deleted file mode 100644 index ec39fa7b55..0000000000 --- a/cli/command/checkpoint/remove.go +++ /dev/null @@ -1,44 +0,0 @@ -package checkpoint - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type removeOptions struct { - checkpointDir string -} - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] CONTAINER CHECKPOINT", - Aliases: []string{"remove"}, - Short: "Remove a checkpoint", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args[0], args[1], opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd -} - -func runRemove(dockerCli *command.DockerCli, container string, checkpoint string, opts removeOptions) error { - client := dockerCli.Client() - - removeOpts := types.CheckpointDeleteOptions{ - CheckpointID: checkpoint, - CheckpointDir: opts.checkpointDir, - } - - return client.CheckpointDelete(context.Background(), container, removeOpts) -} diff --git a/cli/command/cli.go b/cli/command/cli.go deleted file mode 100644 index 82940169ef..0000000000 --- a/cli/command/cli.go +++ /dev/null @@ -1,310 +0,0 @@ -package command - -import ( - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "runtime" - - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/versions" - cliconfig "github.com/docker/docker/cli/config" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/config/credentials" - cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/client" - "github.com/docker/docker/dockerversion" - dopts "github.com/docker/docker/opts" - "github.com/docker/go-connections/sockets" - "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary/passphrase" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// Streams is an interface which exposes the standard input and output streams -type Streams interface { - In() *InStream - Out() *OutStream - Err() io.Writer -} - -// Cli represents the docker command line client. -type Cli interface { - Client() client.APIClient - Out() *OutStream - Err() io.Writer - In() *InStream - SetIn(in *InStream) - ConfigFile() *configfile.ConfigFile - CredentialsStore(serverAddress string) credentials.Store -} - -// DockerCli is an instance the docker command line client. -// Instances of the client can be returned from NewDockerCli. -type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient - defaultVersion string - server ServerInfo -} - -// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. -func (cli *DockerCli) DefaultVersion() string { - return cli.defaultVersion -} - -// Client returns the APIClient -func (cli *DockerCli) Client() client.APIClient { - return cli.client -} - -// Out returns the writer used for stdout -func (cli *DockerCli) Out() *OutStream { - return cli.out -} - -// Err returns the writer used for stderr -func (cli *DockerCli) Err() io.Writer { - return cli.err -} - -// SetIn sets the reader used for stdin -func (cli *DockerCli) SetIn(in *InStream) { - cli.in = in -} - -// In returns the reader used for stdin -func (cli *DockerCli) In() *InStream { - return cli.in -} - -// ShowHelp shows the command help. -func (cli *DockerCli) ShowHelp(cmd *cobra.Command, args []string) error { - cmd.SetOutput(cli.err) - cmd.HelpFunc()(cmd, args) - return nil -} - -// ConfigFile returns the ConfigFile -func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { - return cli.configFile -} - -// ServerInfo returns the server version details for the host this client is -// connected to -func (cli *DockerCli) ServerInfo() ServerInfo { - return cli.server -} - -// GetAllCredentials returns all of the credentials stored in all of the -// configured credential stores. -func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { - auths := make(map[string]types.AuthConfig) - for registry := range cli.configFile.CredentialHelpers { - helper := cli.CredentialsStore(registry) - newAuths, err := helper.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - } - defaultStore := cli.CredentialsStore("") - newAuths, err := defaultStore.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - return auths, nil -} - -func addAll(to, from map[string]types.AuthConfig) { - for reg, ac := range from { - to[reg] = ac - } -} - -// CredentialsStore returns a new credentials store based -// on the settings provided in the configuration file. Empty string returns -// the default credential store. -func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store { - if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" { - return credentials.NewNativeStore(cli.configFile, helper) - } - return credentials.NewFileStore(cli.configFile) -} - -// getConfiguredCredentialStore returns the credential helper configured for the -// given registry, the default credsStore, or the empty string if neither are -// configured. -func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string { - if c.CredentialHelpers != nil && serverAddress != "" { - if helper, exists := c.CredentialHelpers[serverAddress]; exists { - return helper - } - } - return c.CredentialsStore -} - -// Initialize the dockerCli runs initialization that must happen after command -// line flags are parsed. -func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { - cli.configFile = LoadDefaultConfigFile(cli.err) - - var err error - cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) - if tlsconfig.IsErrEncryptedKey(err) { - var ( - passwd string - giveup bool - ) - passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) - - for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ { - // some code and comments borrowed from notary/trustmanager/keystore.go - passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts) - // Check if the passphrase retriever got an error or if it is telling us to give up - if giveup || err != nil { - return errors.Wrap(err, "private key is encrypted, but could not get passphrase") - } - - opts.Common.TLSOptions.Passphrase = passwd - cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) - } - } - - if err != nil { - return err - } - - cli.defaultVersion = cli.client.ClientVersion() - - if opts.Common.TrustKey == "" { - cli.keyFile = filepath.Join(cliconfig.Dir(), cliflags.DefaultTrustKeyFile) - } else { - cli.keyFile = opts.Common.TrustKey - } - - if ping, err := cli.client.Ping(context.Background()); err == nil { - cli.server = ServerInfo{ - HasExperimental: ping.Experimental, - OSType: ping.OSType, - } - - // since the new header was added in 1.25, assume server is 1.24 if header is not present. - if ping.APIVersion == "" { - ping.APIVersion = "1.24" - } - - // if server version is lower than the current cli, downgrade - if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) { - cli.client.UpdateClientVersion(ping.APIVersion) - } - } - - return nil -} - -// ServerInfo stores details about the supported features and platform of the -// server -type ServerInfo struct { - HasExperimental bool - OSType string -} - -// 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) *DockerCli { - return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} -} - -// LoadDefaultConfigFile attempts to load the default config file and returns -// an initialized ConfigFile struct if none is found. -func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { - configFile, e := cliconfig.Load(cliconfig.Dir()) - if e != nil { - fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) - } - if !configFile.ContainsAuth() { - credentials.DetectDefaultStore(configFile) - } - return configFile -} - -// NewAPIClientFromFlags creates a new APIClient from command line flags -func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { - host, err := getServerHost(opts.Hosts, opts.TLSOptions) - if err != nil { - return &client.Client{}, err - } - - customHeaders := configFile.HTTPHeaders - if customHeaders == nil { - customHeaders = map[string]string{} - } - customHeaders["User-Agent"] = UserAgent() - - verStr := api.DefaultVersion - if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { - verStr = tmpStr - } - - httpClient, err := newHTTPClient(host, opts.TLSOptions) - if err != nil { - return &client.Client{}, err - } - - return client.NewClient(host, verStr, httpClient, customHeaders) -} - -func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { - switch len(hosts) { - case 0: - host = os.Getenv("DOCKER_HOST") - case 1: - host = hosts[0] - default: - return "", errors.New("Please specify only one -H") - } - - host, err = dopts.ParseHost(tlsOptions != nil, host) - return -} - -func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, error) { - if tlsOptions == nil { - // let the api client configure the default transport. - return nil, nil - } - opts := *tlsOptions - opts.ExclusiveRootPools = true - config, err := tlsconfig.Client(opts) - if err != nil { - return nil, err - } - tr := &http.Transport{ - TLSClientConfig: config, - } - proto, addr, _, err := client.ParseHost(host) - if err != nil { - return nil, err - } - - sockets.ConfigureTransport(tr, proto, addr) - - return &http.Client{ - Transport: tr, - }, nil -} - -// UserAgent returns the user agent string used for making API requests -func UserAgent() string { - return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" -} diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go deleted file mode 100644 index 0db7f3a409..0000000000 --- a/cli/command/commands/commands.go +++ /dev/null @@ -1,121 +0,0 @@ -package commands - -import ( - "os" - - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/checkpoint" - "github.com/docker/docker/cli/command/container" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/cli/command/network" - "github.com/docker/docker/cli/command/node" - "github.com/docker/docker/cli/command/plugin" - "github.com/docker/docker/cli/command/registry" - "github.com/docker/docker/cli/command/secret" - "github.com/docker/docker/cli/command/service" - "github.com/docker/docker/cli/command/stack" - "github.com/docker/docker/cli/command/swarm" - "github.com/docker/docker/cli/command/system" - "github.com/docker/docker/cli/command/volume" - "github.com/spf13/cobra" -) - -// AddCommands adds all the commands from cli/command to the root command -func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { - cmd.AddCommand( - // checkpoint - checkpoint.NewCheckpointCommand(dockerCli), - - // container - container.NewContainerCommand(dockerCli), - container.NewRunCommand(dockerCli), - - // image - image.NewImageCommand(dockerCli), - image.NewBuildCommand(dockerCli), - - // node - node.NewNodeCommand(dockerCli), - - // network - network.NewNetworkCommand(dockerCli), - - // plugin - plugin.NewPluginCommand(dockerCli), - - // registry - registry.NewLoginCommand(dockerCli), - registry.NewLogoutCommand(dockerCli), - registry.NewSearchCommand(dockerCli), - - // secret - secret.NewSecretCommand(dockerCli), - - // service - service.NewServiceCommand(dockerCli), - - // system - system.NewSystemCommand(dockerCli), - system.NewVersionCommand(dockerCli), - - // stack - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - - // swarm - swarm.NewSwarmCommand(dockerCli), - - // volume - volume.NewVolumeCommand(dockerCli), - - // legacy commands may be hidden - hide(system.NewEventsCommand(dockerCli)), - hide(system.NewInfoCommand(dockerCli)), - hide(system.NewInspectCommand(dockerCli)), - hide(container.NewAttachCommand(dockerCli)), - hide(container.NewCommitCommand(dockerCli)), - hide(container.NewCopyCommand(dockerCli)), - hide(container.NewCreateCommand(dockerCli)), - hide(container.NewDiffCommand(dockerCli)), - hide(container.NewExecCommand(dockerCli)), - hide(container.NewExportCommand(dockerCli)), - hide(container.NewKillCommand(dockerCli)), - hide(container.NewLogsCommand(dockerCli)), - hide(container.NewPauseCommand(dockerCli)), - hide(container.NewPortCommand(dockerCli)), - hide(container.NewPsCommand(dockerCli)), - hide(container.NewRenameCommand(dockerCli)), - hide(container.NewRestartCommand(dockerCli)), - hide(container.NewRmCommand(dockerCli)), - hide(container.NewStartCommand(dockerCli)), - hide(container.NewStatsCommand(dockerCli)), - hide(container.NewStopCommand(dockerCli)), - hide(container.NewTopCommand(dockerCli)), - hide(container.NewUnpauseCommand(dockerCli)), - hide(container.NewUpdateCommand(dockerCli)), - hide(container.NewWaitCommand(dockerCli)), - hide(image.NewHistoryCommand(dockerCli)), - hide(image.NewImagesCommand(dockerCli)), - hide(image.NewImportCommand(dockerCli)), - hide(image.NewLoadCommand(dockerCli)), - hide(image.NewPullCommand(dockerCli)), - hide(image.NewPushCommand(dockerCli)), - hide(image.NewRemoveCommand(dockerCli)), - hide(image.NewSaveCommand(dockerCli)), - hide(image.NewTagCommand(dockerCli)), - ) - -} - -func hide(cmd *cobra.Command) *cobra.Command { - // If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty, - // these legacy commands (such as `docker ps`, `docker exec`, etc) - // will not be shown in output console. - if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" { - return cmd - } - cmdCopy := *cmd - cmdCopy.Hidden = true - cmdCopy.Aliases = []string{} - return &cmdCopy -} diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go deleted file mode 100644 index 0564bdcd0f..0000000000 --- a/cli/command/container/attach.go +++ /dev/null @@ -1,129 +0,0 @@ -package container - -import ( - "io" - "net/http/httputil" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/signal" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type attachOptions struct { - noStdin bool - proxy bool - detachKeys string - - container string -} - -// NewAttachCommand creates a new cobra.Command for `docker attach` -func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts attachOptions - - cmd := &cobra.Command{ - Use: "attach [OPTIONS] CONTAINER", - Short: "Attach local standard input, output, and error streams to a running container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runAttach(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN") - flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - return cmd -} - -func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - c, err := client.ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if !c.State.Running { - return errors.New("You cannot attach to a stopped container, start it first") - } - - if c.State.Paused { - return errors.New("You cannot attach to a paused container, unpause it first") - } - - if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil { - return err - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: !opts.noStdin && c.Config.OpenStdin, - Stdout: true, - Stderr: true, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - var in io.ReadCloser - if options.Stdin { - in = dockerCli.In() - } - - if opts.proxy && !c.Config.Tty { - sigc := ForwardAllSignals(ctx, dockerCli, opts.container) - defer signal.StopCatch(sigc) - } - - resp, errAttach := client.ContainerAttach(ctx, opts.container, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach returns an ErrPersistEOF (connection closed) - // means server met an error and put it in Hijacked connection - // keep the error and read detailed error message from hijacked connection later - return errAttach - } - defer resp.Close() - - if c.Config.Tty && dockerCli.Out().IsTerminal() { - height, width := dockerCli.Out().GetTtySize() - // To handle the case where a user repeatedly attaches/detaches without resizing their - // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially - // resize it, then go back to normal. Without this, every attach after the first will - // require the user to manually resize or hit enter. - resizeTtyTo(ctx, client, opts.container, height+1, width+1, false) - - // After the above resizing occurs, the call to MonitorTtySize below will handle resetting back - // to the actual size. - if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil { - logrus.Debugf("Error monitoring TTY size: %s", err) - } - } - if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil { - return err - } - - if errAttach != nil { - return errAttach - } - - _, status, err := getExitCode(ctx, dockerCli, opts.container) - if err != nil { - return err - } - if status != 0 { - return cli.StatusError{StatusCode: status} - } - - return nil -} diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go deleted file mode 100644 index b78411e0a3..0000000000 --- a/cli/command/container/cmd.go +++ /dev/null @@ -1,45 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewContainerCommand returns a cobra command for `container` subcommands -func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "container", - Short: "Manage containers", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewAttachCommand(dockerCli), - NewCommitCommand(dockerCli), - NewCopyCommand(dockerCli), - NewCreateCommand(dockerCli), - NewDiffCommand(dockerCli), - NewExecCommand(dockerCli), - NewExportCommand(dockerCli), - NewKillCommand(dockerCli), - NewLogsCommand(dockerCli), - NewPauseCommand(dockerCli), - NewPortCommand(dockerCli), - NewRenameCommand(dockerCli), - NewRestartCommand(dockerCli), - NewRmCommand(dockerCli), - NewRunCommand(dockerCli), - NewStartCommand(dockerCli), - NewStatsCommand(dockerCli), - NewStopCommand(dockerCli), - NewTopCommand(dockerCli), - NewUnpauseCommand(dockerCli), - NewUpdateCommand(dockerCli), - NewWaitCommand(dockerCli), - newListCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/container/commit.go b/cli/command/container/commit.go deleted file mode 100644 index 8f67d96d87..0000000000 --- a/cli/command/container/commit.go +++ /dev/null @@ -1,75 +0,0 @@ -package container - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - dockeropts "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type commitOptions struct { - container string - reference string - - pause bool - comment string - author string - changes dockeropts.ListOpts -} - -// NewCommitCommand creates a new cobra.Command for `docker commit` -func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts commitOptions - - cmd := &cobra.Command{ - Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]", - Short: "Create a new image from a container's changes", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - if len(args) > 1 { - opts.reference = args[1] - } - return runCommit(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.BoolVarP(&opts.pause, "pause", "p", true, "Pause container during commit") - flags.StringVarP(&opts.comment, "message", "m", "", "Commit message") - flags.StringVarP(&opts.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith \")") - - opts.changes = dockeropts.NewListOpts(nil) - flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") - - return cmd -} - -func runCommit(dockerCli *command.DockerCli, opts *commitOptions) error { - ctx := context.Background() - - name := opts.container - reference := opts.reference - - options := types.ContainerCommitOptions{ - Reference: reference, - Comment: opts.comment, - Author: opts.author, - Changes: opts.changes.GetAll(), - Pause: opts.pause, - } - - response, err := dockerCli.Client().ContainerCommit(ctx, name, options) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), response.ID) - return nil -} diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go deleted file mode 100644 index a4165a18d2..0000000000 --- a/cli/command/container/cp.go +++ /dev/null @@ -1,305 +0,0 @@ -package container - -import ( - "io" - "os" - "path/filepath" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/system" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type copyOptions struct { - source string - destination string - followLink bool - copyUIDGID bool -} - -type copyDirection int - -const ( - fromContainer copyDirection = (1 << iota) - toContainer - acrossContainers = fromContainer | toContainer -) - -type cpConfig struct { - followLink bool -} - -// NewCopyCommand creates a new `docker cp` command -func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts copyOptions - - cmd := &cobra.Command{ - Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- - docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, - Short: "Copy files/folders between a container and the local filesystem", - Long: strings.Join([]string{ - "Copy files/folders between a container and the local filesystem\n", - "\nUse '-' as the source to read a tar archive from stdin\n", - "and extract it to a directory destination in a container.\n", - "Use '-' as the destination to stream a tar archive of a\n", - "container source to stdout.", - }, ""), - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - if args[0] == "" { - return errors.New("source can not be empty") - } - if args[1] == "" { - return errors.New("destination can not be empty") - } - opts.source = args[0] - opts.destination = args[1] - return runCopy(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") - flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") - - return cmd -} - -func runCopy(dockerCli *command.DockerCli, opts copyOptions) error { - srcContainer, srcPath := splitCpArg(opts.source) - dstContainer, dstPath := splitCpArg(opts.destination) - - var direction copyDirection - if srcContainer != "" { - direction |= fromContainer - } - if dstContainer != "" { - direction |= toContainer - } - - cpParam := &cpConfig{ - followLink: opts.followLink, - } - - ctx := context.Background() - - switch direction { - case fromContainer: - return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam) - case toContainer: - return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID) - case acrossContainers: - // Copying between containers isn't supported. - return errors.New("copying between containers is not supported") - default: - // User didn't specify any container. - return errors.New("must specify at least one container source") - } -} - -func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, containerName, path string) (types.ContainerPathStat, error) { - return dockerCli.Client().ContainerStatPath(ctx, containerName, path) -} - -func resolveLocalPath(localPath string) (absPath string, err error) { - if absPath, err = filepath.Abs(localPath); err != nil { - return - } - - return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil -} - -func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { - if dstPath != "-" { - // Get an absolute destination path. - dstPath, err = resolveLocalPath(dstPath) - if err != nil { - return err - } - } - - // if client requests to follow symbol link, then must decide target file to be copied - var rebaseName string - if cpParam.followLink { - srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath) - - // If the destination is a symbolic link, we should follow it. - if err == nil && srcStat.Mode&os.ModeSymlink != 0 { - linkTarget := srcStat.LinkTarget - if !system.IsAbs(linkTarget) { - // Join with the parent directory. - srcParent, _ := archive.SplitPathDirEntry(srcPath) - linkTarget = filepath.Join(srcParent, linkTarget) - } - - linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) - srcPath = linkTarget - } - - } - - content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath) - if err != nil { - return err - } - defer content.Close() - - if dstPath == "-" { - // Send the response to STDOUT. - _, err = io.Copy(os.Stdout, content) - - return err - } - - // Prepare source copy info. - srcInfo := archive.CopyInfo{ - Path: srcPath, - Exists: true, - IsDir: stat.Mode.IsDir(), - RebaseName: rebaseName, - } - - preArchive := content - if len(srcInfo.RebaseName) != 0 { - _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) - preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) - } - // See comments in the implementation of `archive.CopyTo` for exactly what - // goes into deciding how and whether the source archive needs to be - // altered for the correct copy behavior. - return archive.CopyTo(preArchive, srcInfo, dstPath) -} - -func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) { - if srcPath != "-" { - // Get an absolute source path. - srcPath, err = resolveLocalPath(srcPath) - if err != nil { - return err - } - } - - // In order to get the copy behavior right, we need to know information - // about both the source and destination. The API is a simple tar - // archive/extract API but we can use the stat info header about the - // destination to be more informed about exactly what the destination is. - - // Prepare destination copy info by stat-ing the container path. - dstInfo := archive.CopyInfo{Path: dstPath} - dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath) - - // If the destination is a symbolic link, we should evaluate it. - if err == nil && dstStat.Mode&os.ModeSymlink != 0 { - linkTarget := dstStat.LinkTarget - if !system.IsAbs(linkTarget) { - // Join with the parent directory. - dstParent, _ := archive.SplitPathDirEntry(dstPath) - linkTarget = filepath.Join(dstParent, linkTarget) - } - - dstInfo.Path = linkTarget - dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget) - } - - // Ignore any error and assume that the parent directory of the destination - // path exists, in which case the copy may still succeed. If there is any - // type of conflict (e.g., non-directory overwriting an existing directory - // or vice versa) the extraction will fail. If the destination simply did - // not exist, but the parent directory does, the extraction will still - // succeed. - if err == nil { - dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() - } - - var ( - content io.Reader - resolvedDstPath string - ) - - if srcPath == "-" { - // Use STDIN. - content = os.Stdin - resolvedDstPath = dstInfo.Path - if !dstInfo.IsDir { - return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) - } - } else { - // Prepare source copy info. - srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) - if err != nil { - return err - } - - srcArchive, err := archive.TarResource(srcInfo) - if err != nil { - return err - } - defer srcArchive.Close() - - // With the stat info about the local source as well as the - // destination, we have enough information to know whether we need to - // alter the archive that we upload so that when the server extracts - // it to the specified directory in the container we get the desired - // copy behavior. - - // See comments in the implementation of `archive.PrepareArchiveCopy` - // for exactly what goes into deciding how and whether the source - // archive needs to be altered for the correct copy behavior when it is - // extracted. This function also infers from the source and destination - // info which directory to extract to, which may be the parent of the - // destination that the user specified. - dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) - if err != nil { - return err - } - defer preparedArchive.Close() - - resolvedDstPath = dstDir - content = preparedArchive - } - - options := types.CopyToContainerOptions{ - AllowOverwriteDirWithFile: false, - CopyUIDGID: copyUIDGID, - } - - return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options) -} - -// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be -// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by -// requiring a LOCALPATH with a `:` to be made explicit with a relative or -// absolute path: -// `/path/to/file:name.txt` or `./file:name.txt` -// -// This is apparently how `scp` handles this as well: -// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ -// -// We can't simply check for a filepath separator because container names may -// have a separator, e.g., "host0/cname1" if container is in a Docker cluster, -// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows -// client, a `:` could be part of an absolute Windows path, in which case it -// is immediately proceeded by a backslash. -func splitCpArg(arg string) (container, path string) { - if system.IsAbs(arg) { - // Explicit local absolute path, e.g., `C:\foo` or `/foo`. - return "", arg - } - - parts := strings.SplitN(arg, ":", 2) - - if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { - // Either there's no `:` in the arg - // OR it's an explicit local relative path like `./file:name.txt`. - return "", arg - } - - return parts[0], parts[1] -} diff --git a/cli/command/container/create.go b/cli/command/container/create.go deleted file mode 100644 index 9222b4060b..0000000000 --- a/cli/command/container/create.go +++ /dev/null @@ -1,224 +0,0 @@ -package container - -import ( - "fmt" - "io" - "os" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - apiclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type createOptions struct { - name string -} - -// NewCreateCommand creates a new cobra.Command for `docker create` -func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts createOptions - var copts *containerOptions - - cmd := &cobra.Command{ - Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Create a new container", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - copts.Image = args[0] - if len(args) > 1 { - copts.Args = args[1:] - } - return runCreate(dockerCli, cmd.Flags(), &opts, copts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.StringVar(&opts.name, "name", "", "Assign a name to the container") - - // Add an explicit help that doesn't have a `-h` to prevent the conflict - // with hostname - flags.Bool("help", false, "Print usage") - - command.AddTrustVerificationFlags(flags) - copts = addFlags(flags) - return cmd -} - -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *containerOptions) error { - containerConfig, err := parse(flags, copts) - if err != nil { - reportError(dockerCli.Err(), "create", err.Error(), true) - return cli.StatusError{StatusCode: 125} - } - response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name) - if err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), response.ID) - return nil -} - -func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error { - ref, err := reference.ParseNormalizedNamed(image) - if err != nil { - return err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageCreateOptions{ - RegistryAuth: encodedAuth, - } - - responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesStream( - responseBody, - out, - dockerCli.Out().FD(), - dockerCli.Out().IsTerminal(), - nil) -} - -type cidFile struct { - path string - file *os.File - written bool -} - -func (cid *cidFile) Close() error { - cid.file.Close() - - if cid.written { - return nil - } - if err := os.Remove(cid.path); err != nil { - return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) - } - - return nil -} - -func (cid *cidFile) Write(id string) error { - if _, err := cid.file.Write([]byte(id)); err != nil { - return errors.Errorf("Failed to write the container ID to the file: %s", err) - } - cid.written = true - return nil -} - -func newCIDFile(path string) (*cidFile, error) { - if _, err := os.Stat(path); err == nil { - return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) - } - - f, err := os.Create(path) - if err != nil { - return nil, errors.Errorf("Failed to create the container ID file: %s", err) - } - - return &cidFile{path: path, file: f}, nil -} - -func createContainer(ctx context.Context, dockerCli *command.DockerCli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) { - config := containerConfig.Config - hostConfig := containerConfig.HostConfig - networkingConfig := containerConfig.NetworkingConfig - stderr := dockerCli.Err() - - var ( - containerIDFile *cidFile - trustedRef reference.Canonical - namedRef reference.Named - ) - - cidfile := hostConfig.ContainerIDFile - if cidfile != "" { - var err error - if containerIDFile, err = newCIDFile(cidfile); err != nil { - return nil, err - } - defer containerIDFile.Close() - } - - ref, err := reference.ParseAnyReference(config.Image) - if err != nil { - return nil, err - } - if named, ok := ref.(reference.Named); ok { - namedRef = reference.TagNameOnly(named) - - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && command.IsTrusted() { - var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil) - if err != nil { - return nil, err - } - config.Image = reference.FamiliarString(trustedRef) - } - } - - //create the container - response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - - //if image not found try to pull it - if err != nil { - if apiclient.IsErrImageNotFound(err) && namedRef != nil { - fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) - - // we don't want to write to stdout anything apart from container.ID - if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil { - return nil, err - } - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { - if err := image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef); err != nil { - return nil, err - } - } - // Retry - var retryErr error - response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - if retryErr != nil { - return nil, retryErr - } - } else { - return nil, err - } - } - - for _, warning := range response.Warnings { - fmt.Fprintf(stderr, "WARNING: %s\n", warning) - } - if containerIDFile != nil { - if err = containerIDFile.Write(response.ID); err != nil { - return nil, err - } - } - return &response, nil -} diff --git a/cli/command/container/diff.go b/cli/command/container/diff.go deleted file mode 100644 index 816a0a56a3..0000000000 --- a/cli/command/container/diff.go +++ /dev/null @@ -1,46 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type diffOptions struct { - container string -} - -// NewDiffCommand creates a new cobra.Command for `docker diff` -func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts diffOptions - - return &cobra.Command{ - Use: "diff CONTAINER", - Short: "Inspect changes to files or directories on a container's filesystem", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runDiff(dockerCli, &opts) - }, - } -} - -func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { - if opts.container == "" { - return errors.New("Container name cannot be empty") - } - ctx := context.Background() - - changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container) - if err != nil { - return err - } - diffCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewDiffFormat("{{.Type}} {{.Path}}"), - } - return formatter.DiffWrite(diffCtx, changes) -} diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go deleted file mode 100644 index 676708c77b..0000000000 --- a/cli/command/container/exec.go +++ /dev/null @@ -1,205 +0,0 @@ -package container - -import ( - "fmt" - "io" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - apiclient "github.com/docker/docker/client" - options "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/promise" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type execOptions struct { - detachKeys string - interactive bool - tty bool - detach bool - user string - privileged bool - env *options.ListOpts -} - -func newExecOptions() *execOptions { - var values []string - return &execOptions{ - env: options.NewListOptsRef(&values, options.ValidateEnv), - } -} - -// NewExecCommand creates a new cobra.Command for `docker exec` -func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := newExecOptions() - - cmd := &cobra.Command{ - Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", - Short: "Run a command in a running container", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - container := args[0] - execCmd := args[1:] - return runExec(dockerCli, opts, container, execCmd) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") - flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached") - flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY") - flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background") - flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") - flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") - flags.VarP(opts.env, "env", "e", "Set environment variables") - flags.SetAnnotation("env", "version", []string{"1.25"}) - - return cmd -} - -func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error { - execConfig, err := parseExec(opts, execCmd) - // just in case the ParseExec does not exit - if container == "" || err != nil { - return cli.StatusError{StatusCode: 1} - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - // Send client escape keys - execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys - - ctx := context.Background() - client := dockerCli.Client() - - response, err := client.ContainerExecCreate(ctx, container, *execConfig) - if err != nil { - return err - } - - execID := response.ID - if execID == "" { - fmt.Fprintln(dockerCli.Out(), "exec ID empty") - return nil - } - - //Temp struct for execStart so that we don't need to transfer all the execConfig - if !execConfig.Detach { - if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { - return err - } - } else { - execStartCheck := types.ExecStartCheck{ - Detach: execConfig.Detach, - Tty: execConfig.Tty, - } - - if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil { - return err - } - // For now don't print this - wait for when we support exec wait() - // fmt.Fprintf(dockerCli.Out(), "%s\n", execID) - return nil - } - - // Interactive exec requested. - var ( - out, stderr io.Writer - in io.ReadCloser - errCh chan error - ) - - if execConfig.AttachStdin { - in = dockerCli.In() - } - if execConfig.AttachStdout { - out = dockerCli.Out() - } - if execConfig.AttachStderr { - if execConfig.Tty { - stderr = dockerCli.Out() - } else { - stderr = dockerCli.Err() - } - } - - resp, err := client.ContainerExecAttach(ctx, execID, *execConfig) - if err != nil { - return err - } - defer resp.Close() - errCh = promise.Go(func() error { - return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp) - }) - - if execConfig.Tty && dockerCli.In().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { - fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) - } - } - - if err := <-errCh; err != nil { - logrus.Debugf("Error hijack: %s", err) - return err - } - - var status int - if _, status, err = getExecExitCode(ctx, client, execID); err != nil { - return err - } - - if status != 0 { - return cli.StatusError{StatusCode: status} - } - - return nil -} - -// getExecExitCode perform an inspect on the exec command. It returns -// the running state and the exit code. -func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) { - resp, err := client.ContainerExecInspect(ctx, execID) - if err != nil { - // If we can't connect, then the daemon probably died. - if !apiclient.IsErrConnectionFailed(err) { - return false, -1, err - } - return false, -1, nil - } - - return resp.Running, resp.ExitCode, nil -} - -// parseExec parses the specified args for the specified command and generates -// an ExecConfig from it. -func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) { - execConfig := &types.ExecConfig{ - User: opts.user, - Privileged: opts.privileged, - Tty: opts.tty, - Cmd: execCmd, - Detach: opts.detach, - } - - // If -d is not set, attach to everything by default - if !opts.detach { - execConfig.AttachStdout = true - execConfig.AttachStderr = true - if opts.interactive { - execConfig.AttachStdin = true - } - } - - if opts.env != nil { - execConfig.Env = opts.env.GetAll() - } - - return execConfig, nil -} diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go deleted file mode 100644 index e94beb4545..0000000000 --- a/cli/command/container/exec_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/api/types" -) - -type arguments struct { - options execOptions - execCmd []string -} - -func TestParseExec(t *testing.T) { - valids := map[*arguments]*types.ExecConfig{ - { - execCmd: []string{"command"}, - }: { - Cmd: []string{"command"}, - AttachStdout: true, - AttachStderr: true, - }, - { - execCmd: []string{"command1", "command2"}, - }: { - Cmd: []string{"command1", "command2"}, - AttachStdout: true, - AttachStderr: true, - }, - { - options: execOptions{ - interactive: true, - tty: true, - user: "uid", - }, - execCmd: []string{"command"}, - }: { - User: "uid", - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: true, - Cmd: []string{"command"}, - }, - { - options: execOptions{ - detach: true, - }, - execCmd: []string{"command"}, - }: { - AttachStdin: false, - AttachStdout: false, - AttachStderr: false, - Detach: true, - Cmd: []string{"command"}, - }, - { - options: execOptions{ - tty: true, - interactive: true, - detach: true, - }, - execCmd: []string{"command"}, - }: { - AttachStdin: false, - AttachStdout: false, - AttachStderr: false, - Detach: true, - Tty: true, - Cmd: []string{"command"}, - }, - } - - for valid, expectedExecConfig := range valids { - execConfig, err := parseExec(&valid.options, valid.execCmd) - if err != nil { - t.Fatal(err) - } - if !compareExecConfig(expectedExecConfig, execConfig) { - t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig) - } - } -} - -func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool { - if config1.AttachStderr != config2.AttachStderr { - return false - } - if config1.AttachStdin != config2.AttachStdin { - return false - } - if config1.AttachStdout != config2.AttachStdout { - return false - } - if config1.Detach != config2.Detach { - return false - } - if config1.Privileged != config2.Privileged { - return false - } - if config1.Tty != config2.Tty { - return false - } - if config1.User != config2.User { - return false - } - if len(config1.Cmd) != len(config2.Cmd) { - return false - } - for index, value := range config1.Cmd { - if value != config2.Cmd[index] { - return false - } - } - return true -} diff --git a/cli/command/container/export.go b/cli/command/container/export.go deleted file mode 100644 index cb0ddfe7a7..0000000000 --- a/cli/command/container/export.go +++ /dev/null @@ -1,58 +0,0 @@ -package container - -import ( - "io" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type exportOptions struct { - container string - output string -} - -// NewExportCommand creates a new `docker export` command -func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts exportOptions - - cmd := &cobra.Command{ - Use: "export [OPTIONS] CONTAINER", - Short: "Export a container's filesystem as a tar archive", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runExport(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - - return cmd -} - -func runExport(dockerCli *command.DockerCli, opts exportOptions) error { - if opts.output == "" && dockerCli.Out().IsTerminal() { - return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") - } - - clnt := dockerCli.Client() - - responseBody, err := clnt.ContainerExport(context.Background(), opts.container) - if err != nil { - return err - } - defer responseBody.Close() - - if opts.output == "" { - _, err := io.Copy(dockerCli.Out(), responseBody) - return err - } - - return command.CopyToFile(opts.output, responseBody) -} diff --git a/cli/command/container/hijack.go b/cli/command/container/hijack.go deleted file mode 100644 index 11acf114f0..0000000000 --- a/cli/command/container/hijack.go +++ /dev/null @@ -1,124 +0,0 @@ -package container - -import ( - "io" - "runtime" - "sync" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stdcopy" - "golang.org/x/net/context" -) - -// holdHijackedConnection handles copying input to and output from streams to the -// connection -func holdHijackedConnection(ctx context.Context, streams command.Streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { - var ( - err error - restoreOnce sync.Once - ) - if inputStream != nil && tty { - if err := setRawTerminal(streams); err != nil { - return err - } - defer func() { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - }() - } - - receiveStdout := make(chan error, 1) - if outputStream != nil || errorStream != nil { - go func() { - // When TTY is ON, use regular copy - if tty && outputStream != nil { - _, err = io.Copy(outputStream, resp.Reader) - // we should restore the terminal as soon as possible once connection end - // so any following print messages will be in normal type. - if inputStream != nil { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - } - } else { - _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) - } - - logrus.Debug("[hijack] End of stdout") - receiveStdout <- err - }() - } - - stdinDone := make(chan struct{}) - go func() { - if inputStream != nil { - io.Copy(resp.Conn, inputStream) - // we should restore the terminal as soon as possible once connection end - // so any following print messages will be in normal type. - if tty { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - } - logrus.Debug("[hijack] End of stdin") - } - - if err := resp.CloseWrite(); err != nil { - logrus.Debugf("Couldn't send EOF: %s", err) - } - close(stdinDone) - }() - - select { - case err := <-receiveStdout: - if err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - case <-stdinDone: - if outputStream != nil || errorStream != nil { - select { - case err := <-receiveStdout: - if err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - case <-ctx.Done(): - } - } - case <-ctx.Done(): - } - - return nil -} - -func setRawTerminal(streams command.Streams) error { - if err := streams.In().SetRawTerminal(); err != nil { - return err - } - return streams.Out().SetRawTerminal() -} - -func restoreTerminal(streams command.Streams, in io.Closer) error { - streams.In().RestoreTerminal() - streams.Out().RestoreTerminal() - // WARNING: DO NOT REMOVE THE OS CHECKS !!! - // For some reason this Close call blocks on darwin.. - // As the client exits right after, simply discard the close - // until we find a better solution. - // - // This can also cause the client on Windows to get stuck in Win32 CloseHandle() - // in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442 - // Tracked internally at Microsoft by VSO #11352156. In the - // Windows case, you hit this if you are using the native/v2 console, - // not the "legacy" console, and you start the client in a new window. eg - // `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar` - // will hang. Remove start, and it won't repro. - if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { - return in.Close() - } - return nil -} diff --git a/cli/command/container/inspect.go b/cli/command/container/inspect.go deleted file mode 100644 index d08b38dc96..0000000000 --- a/cli/command/container/inspect.go +++ /dev/null @@ -1,46 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - size bool - refs []string -} - -// newInspectCommand creates a new cobra.Command for `docker container inspect` -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Display detailed information on one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRefFunc := func(ref string) (interface{}, []byte, error) { - return client.ContainerInspectWithRaw(ctx, ref, opts.size) - } - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) -} diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go deleted file mode 100644 index 4cc3ee0fcb..0000000000 --- a/cli/command/container/kill.go +++ /dev/null @@ -1,56 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type killOptions struct { - signal string - - containers []string -} - -// NewKillCommand creates a new cobra.Command for `docker kill` -func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts killOptions - - cmd := &cobra.Command{ - Use: "kill [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Kill one or more running containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runKill(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container") - return cmd -} - -func runKill(dockerCli *command.DockerCli, opts *killOptions) error { - var errs []string - ctx := context.Background() - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - return dockerCli.Client().ContainerKill(ctx, container, opts.signal) - }) - for _, name := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - } else { - fmt.Fprintln(dockerCli.Out(), name) - } - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/list.go b/cli/command/container/list.go deleted file mode 100644 index e0f4fdf21f..0000000000 --- a/cli/command/container/list.go +++ /dev/null @@ -1,140 +0,0 @@ -package container - -import ( - "io/ioutil" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type psOptions struct { - quiet bool - size bool - all bool - noTrunc bool - nLatest bool - last int - format string - filter opts.FilterOpt -} - -// NewPsCommand creates a new cobra.Command for `docker ps` -func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS]", - Short: "List containers", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runPs(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display numeric IDs") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)") - flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)") - flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := *NewPsCommand(dockerCli) - cmd.Aliases = []string{"ps", "list"} - cmd.Use = "ls [OPTIONS]" - return &cmd -} - -// listOptionsProcessor is used to set any container list options which may only -// be embedded in the format template. -// This is passed directly into tmpl.Execute in order to allow the preprocessor -// to set any list options that were not provided by flags (e.g. `.Size`). -// It is using a `map[string]bool` so that unknown fields passed into the -// template format do not cause errors. These errors will get picked up when -// running through the actual template processor. -type listOptionsProcessor map[string]bool - -// Size sets the size of the map when called by a template execution. -func (o listOptionsProcessor) Size() bool { - o["size"] = true - return true -} - -// Label is needed here as it allows the correct pre-processing -// because Label() is a method with arguments -func (o listOptionsProcessor) Label(name string) string { - return "" -} - -func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { - options := &types.ContainerListOptions{ - All: opts.all, - Limit: opts.last, - Size: opts.size, - Filters: opts.filter.Value(), - } - - if opts.nLatest && opts.last == -1 { - options.Limit = 1 - } - - tmpl, err := templates.Parse(opts.format) - - if err != nil { - return nil, err - } - - optionsProcessor := listOptionsProcessor{} - // This shouldn't error out but swallowing the error makes it harder - // to track down if preProcessor issues come up. Ref #24696 - if err := tmpl.Execute(ioutil.Discard, optionsProcessor); err != nil { - return nil, err - } - // At the moment all we need is to capture .Size for preprocessor - options.Size = opts.size || optionsProcessor["size"] - - return options, nil -} - -func runPs(dockerCli *command.DockerCli, opts *psOptions) error { - ctx := context.Background() - - listOptions, err := buildContainerListOptions(opts) - if err != nil { - return err - } - - containers, err := dockerCli.Client().ContainerList(ctx, *listOptions) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().PsFormat - } else { - format = formatter.TableFormatKey - } - } - - containerCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size), - Trunc: !opts.noTrunc, - } - return formatter.ContainerWrite(containerCtx, containers) -} diff --git a/cli/command/container/logs.go b/cli/command/container/logs.go deleted file mode 100644 index d8cafaf744..0000000000 --- a/cli/command/container/logs.go +++ /dev/null @@ -1,76 +0,0 @@ -package container - -import ( - "io" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stdcopy" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type logsOptions struct { - follow bool - since string - timestamps bool - details bool - tail string - - container string -} - -// NewLogsCommand creates a new cobra.Command for `docker logs` -func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts logsOptions - - cmd := &cobra.Command{ - Use: "logs [OPTIONS] CONTAINER", - Short: "Fetch the logs of a container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runLogs(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") - flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") - flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") - flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") - return cmd -} - -func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { - ctx := context.Background() - - options := types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: opts.since, - Timestamps: opts.timestamps, - Follow: opts.follow, - Tail: opts.tail, - Details: opts.details, - } - responseBody, err := dockerCli.Client().ContainerLogs(ctx, opts.container, options) - if err != nil { - return err - } - defer responseBody.Close() - - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if c.Config.Tty { - _, err = io.Copy(dockerCli.Out(), responseBody) - } else { - _, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody) - } - return err -} diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go deleted file mode 100644 index 2aaa3e750f..0000000000 --- a/cli/command/container/opts.go +++ /dev/null @@ -1,900 +0,0 @@ -package container - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "path" - "regexp" - "strconv" - "strings" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/strslice" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/signal" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -var ( - deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$") -) - -// containerOptions is a data object with all the options for creating a container -type containerOptions struct { - attach opts.ListOpts - volumes opts.ListOpts - tmpfs opts.ListOpts - mounts opts.MountOpt - blkioWeightDevice opts.WeightdeviceOpt - deviceReadBps opts.ThrottledeviceOpt - deviceWriteBps opts.ThrottledeviceOpt - links opts.ListOpts - aliases opts.ListOpts - linkLocalIPs opts.ListOpts - deviceReadIOps opts.ThrottledeviceOpt - deviceWriteIOps opts.ThrottledeviceOpt - env opts.ListOpts - labels opts.ListOpts - deviceCgroupRules opts.ListOpts - devices opts.ListOpts - ulimits *opts.UlimitOpt - sysctls *opts.MapOpts - publish opts.ListOpts - expose opts.ListOpts - dns opts.ListOpts - dnsSearch opts.ListOpts - dnsOptions opts.ListOpts - extraHosts opts.ListOpts - volumesFrom opts.ListOpts - envFile opts.ListOpts - capAdd opts.ListOpts - capDrop opts.ListOpts - groupAdd opts.ListOpts - securityOpt opts.ListOpts - storageOpt opts.ListOpts - labelsFile opts.ListOpts - loggingOpts opts.ListOpts - privileged bool - pidMode string - utsMode string - usernsMode string - publishAll bool - stdin bool - tty bool - oomKillDisable bool - oomScoreAdj int - containerIDFile string - entrypoint string - hostname string - memory opts.MemBytes - memoryReservation opts.MemBytes - memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes - user string - workingDir string - cpuCount int64 - cpuShares int64 - cpuPercent int64 - cpuPeriod int64 - cpuRealtimePeriod int64 - cpuRealtimeRuntime int64 - cpuQuota int64 - cpus opts.NanoCPUs - cpusetCpus string - cpusetMems string - blkioWeight uint16 - ioMaxBandwidth opts.MemBytes - ioMaxIOps uint64 - swappiness int64 - netMode string - macAddress string - ipv4Address string - ipv6Address string - ipcMode string - pidsLimit int64 - restartPolicy string - readonlyRootfs bool - loggingDriver string - cgroupParent string - volumeDriver string - stopSignal string - stopTimeout int - isolation string - shmSize opts.MemBytes - noHealthcheck bool - healthCmd string - healthInterval time.Duration - healthTimeout time.Duration - healthStartPeriod time.Duration - healthRetries int - runtime string - autoRemove bool - init bool - - Image string - Args []string -} - -// addFlags adds all command line flags that will be used by parse to the FlagSet -func addFlags(flags *pflag.FlagSet) *containerOptions { - copts := &containerOptions{ - aliases: opts.NewListOpts(nil), - attach: opts.NewListOpts(validateAttach), - blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice), - capAdd: opts.NewListOpts(nil), - capDrop: opts.NewListOpts(nil), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOptions: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule), - deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), - deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), - deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), - deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), - devices: opts.NewListOpts(validateDevice), - env: opts.NewListOpts(opts.ValidateEnv), - envFile: opts.NewListOpts(nil), - expose: opts.NewListOpts(nil), - extraHosts: opts.NewListOpts(opts.ValidateExtraHost), - groupAdd: opts.NewListOpts(nil), - labels: opts.NewListOpts(opts.ValidateEnv), - labelsFile: opts.NewListOpts(nil), - linkLocalIPs: opts.NewListOpts(nil), - links: opts.NewListOpts(opts.ValidateLink), - loggingOpts: opts.NewListOpts(nil), - publish: opts.NewListOpts(nil), - securityOpt: opts.NewListOpts(nil), - storageOpt: opts.NewListOpts(nil), - sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl), - tmpfs: opts.NewListOpts(nil), - ulimits: opts.NewUlimitOpt(nil), - volumes: opts.NewListOpts(nil), - volumesFrom: opts.NewListOpts(nil), - } - - // General purpose flags - flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") - flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list") - flags.Var(&copts.devices, "device", "Add a host device to the container") - flags.VarP(&copts.env, "env", "e", "Set environment variables") - flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables") - flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image") - flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join") - flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name") - flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached") - flags.VarP(&copts.labels, "label", "l", "Set meta data on a container") - flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") - flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") - flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") - flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, "Signal to stop a container") - flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") - flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) - flags.Var(copts.sysctls, "sysctl", "Sysctl options") - flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") - flags.Var(copts.ulimits, "ulimit", "Ulimit options") - flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: [:])") - flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container") - flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits") - - // Security - flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities") - flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities") - flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") - flags.Var(&copts.securityOpt, "security-opt", "Security Options") - flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") - - // Network and port publishing flag - flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") - flags.Var(&copts.dns, "dns", "Set custom DNS servers") - // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. - // This is to be consistent with service create/update - flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options") - flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options") - flags.MarkHidden("dns-opt") - flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") - flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") - flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)") - flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") - flags.Var(&copts.links, "link", "Add link to another container") - flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") - flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)") - flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host") - flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports") - // We allow for both "--net" and "--network", although the latter is the recommended way. - flags.StringVar(&copts.netMode, "net", "default", "Connect a container to a network") - flags.StringVar(&copts.netMode, "network", "default", "Connect a container to a network") - flags.MarkHidden("net") - // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way. - flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container") - flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container") - flags.MarkHidden("net-alias") - - // Logging and storage - flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container") - flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container") - flags.Var(&copts.loggingOpts, "log-opt", "Log driver options") - flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container") - flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") - flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") - flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") - flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container") - - // Health-checking - flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") - flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)") - flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") - flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)") - flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)") - flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) - flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") - - // Resource management - flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") - flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)") - flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file") - flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") - flags.SetAnnotation("cpu-count", "ostype", []string{"windows"}) - flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") - flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"}) - flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") - flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") - flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) - flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") - flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) - flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.Var(&copts.cpus, "cpus", "Number of CPUs") - flags.SetAnnotation("cpus", "version", []string{"1.25"}) - flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") - flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") - flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") - flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") - flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)") - flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) - flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") - flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) - flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") - flags.VarP(&copts.memory, "memory", "m", "Memory limit") - flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") - flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") - flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer") - flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)") - flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)") - - // Low-level execution (cgroups, namespaces, ...) - flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") - flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use") - flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology") - flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use") - flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm") - flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") - flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") - - flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") - flags.SetAnnotation("init", "version", []string{"1.25"}) - return copts -} - -type containerConfig struct { - Config *container.Config - HostConfig *container.HostConfig - NetworkingConfig *networktypes.NetworkingConfig -} - -// parse parses the args for the specified command and generates a Config, -// a HostConfig and returns them with the specified command. -// If the specified args are not valid, it will return an error. -func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, error) { - var ( - attachStdin = copts.attach.Get("stdin") - attachStdout = copts.attach.Get("stdout") - attachStderr = copts.attach.Get("stderr") - ) - - // Validate the input mac address - if copts.macAddress != "" { - if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { - return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) - } - } - if copts.stdin { - attachStdin = true - } - // If -a is not set, attach to stdout and stderr - if copts.attach.Len() == 0 { - attachStdout = true - attachStderr = true - } - - var err error - - swappiness := copts.swappiness - if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) - } - - mounts := copts.mounts.Value() - if len(mounts) > 0 && copts.volumeDriver != "" { - logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.") - } - var binds []string - volumes := copts.volumes.GetMap() - // add any bind targets to the list of container volumes - for bind := range copts.volumes.GetMap() { - if arr := volumeSplitN(bind, 2); len(arr) > 1 { - // after creating the bind mount we want to delete it from the copts.volumes values because - // we do not want bind mounts being committed to image configs - binds = append(binds, bind) - // We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if - // there are duplicates entries. - delete(volumes, bind) - } - } - - // Can't evaluate options passed into --tmpfs until we actually mount - tmpfs := make(map[string]string) - for _, t := range copts.tmpfs.GetAll() { - if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { - tmpfs[arr[0]] = arr[1] - } else { - tmpfs[arr[0]] = "" - } - } - - var ( - runCmd strslice.StrSlice - entrypoint strslice.StrSlice - ) - - if len(copts.Args) > 0 { - runCmd = strslice.StrSlice(copts.Args) - } - - if copts.entrypoint != "" { - entrypoint = strslice.StrSlice{copts.entrypoint} - } else if flags.Changed("entrypoint") { - // if `--entrypoint=` is parsed then Entrypoint is reset - entrypoint = []string{""} - } - - ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll()) - if err != nil { - return nil, err - } - - // Merge in exposed ports to the map of published ports - for _, e := range copts.expose.GetAll() { - if strings.Contains(e, ":") { - return nil, errors.Errorf("invalid port format for --expose: %s", e) - } - //support two formats for expose, original format /[] or /[] - proto, port := nat.SplitProtoPort(e) - //parse the start and end port and create a sequence of ports to expose - //if expose a port, the start and end port are the same - start, end, err := nat.ParsePortRange(port) - if err != nil { - return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) - } - for i := start; i <= end; i++ { - p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) - if err != nil { - return nil, err - } - if _, exists := ports[p]; !exists { - ports[p] = struct{}{} - } - } - } - - // parse device mappings - deviceMappings := []container.DeviceMapping{} - for _, device := range copts.devices.GetAll() { - deviceMapping, err := parseDevice(device) - if err != nil { - return nil, err - } - deviceMappings = append(deviceMappings, deviceMapping) - } - - // collect all the environment variables for the container - envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll()) - if err != nil { - return nil, err - } - - // collect all the labels for the container - labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll()) - if err != nil { - return nil, err - } - - ipcMode := container.IpcMode(copts.ipcMode) - if !ipcMode.Valid() { - return nil, errors.Errorf("--ipc: invalid IPC mode") - } - - pidMode := container.PidMode(copts.pidMode) - if !pidMode.Valid() { - return nil, errors.Errorf("--pid: invalid PID mode") - } - - utsMode := container.UTSMode(copts.utsMode) - if !utsMode.Valid() { - return nil, errors.Errorf("--uts: invalid UTS mode") - } - - usernsMode := container.UsernsMode(copts.usernsMode) - if !usernsMode.Valid() { - return nil, errors.Errorf("--userns: invalid USER mode") - } - - restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy) - if err != nil { - return nil, err - } - - loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll()) - if err != nil { - return nil, err - } - - securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll()) - if err != nil { - return nil, err - } - - storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll()) - if err != nil { - return nil, err - } - - // Healthcheck - var healthConfig *container.HealthConfig - haveHealthSettings := copts.healthCmd != "" || - copts.healthInterval != 0 || - copts.healthTimeout != 0 || - copts.healthStartPeriod != 0 || - copts.healthRetries != 0 - if copts.noHealthcheck { - if haveHealthSettings { - return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") - } - test := strslice.StrSlice{"NONE"} - healthConfig = &container.HealthConfig{Test: test} - } else if haveHealthSettings { - var probe strslice.StrSlice - if copts.healthCmd != "" { - args := []string{"CMD-SHELL", copts.healthCmd} - probe = strslice.StrSlice(args) - } - if copts.healthInterval < 0 { - return nil, errors.Errorf("--health-interval cannot be negative") - } - if copts.healthTimeout < 0 { - return nil, errors.Errorf("--health-timeout cannot be negative") - } - if copts.healthRetries < 0 { - return nil, errors.Errorf("--health-retries cannot be negative") - } - if copts.healthStartPeriod < 0 { - return nil, fmt.Errorf("--health-start-period cannot be negative") - } - - healthConfig = &container.HealthConfig{ - Test: probe, - Interval: copts.healthInterval, - Timeout: copts.healthTimeout, - StartPeriod: copts.healthStartPeriod, - Retries: copts.healthRetries, - } - } - - resources := container.Resources{ - CgroupParent: copts.cgroupParent, - Memory: copts.memory.Value(), - MemoryReservation: copts.memoryReservation.Value(), - MemorySwap: copts.memorySwap.Value(), - MemorySwappiness: &copts.swappiness, - KernelMemory: copts.kernelMemory.Value(), - OomKillDisable: &copts.oomKillDisable, - NanoCPUs: copts.cpus.Value(), - CPUCount: copts.cpuCount, - CPUPercent: copts.cpuPercent, - CPUShares: copts.cpuShares, - CPUPeriod: copts.cpuPeriod, - CpusetCpus: copts.cpusetCpus, - CpusetMems: copts.cpusetMems, - CPUQuota: copts.cpuQuota, - CPURealtimePeriod: copts.cpuRealtimePeriod, - CPURealtimeRuntime: copts.cpuRealtimeRuntime, - PidsLimit: copts.pidsLimit, - BlkioWeight: copts.blkioWeight, - BlkioWeightDevice: copts.blkioWeightDevice.GetList(), - BlkioDeviceReadBps: copts.deviceReadBps.GetList(), - BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(), - BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), - BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), - IOMaximumIOps: copts.ioMaxIOps, - IOMaximumBandwidth: uint64(copts.ioMaxBandwidth), - Ulimits: copts.ulimits.GetList(), - DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), - Devices: deviceMappings, - } - - config := &container.Config{ - Hostname: copts.hostname, - ExposedPorts: ports, - User: copts.user, - Tty: copts.tty, - // TODO: deprecated, it comes from -n, --networking - // it's still needed internally to set the network to disabled - // if e.g. bridge is none in daemon opts, and in inspect - NetworkDisabled: false, - OpenStdin: copts.stdin, - AttachStdin: attachStdin, - AttachStdout: attachStdout, - AttachStderr: attachStderr, - Env: envVariables, - Cmd: runCmd, - Image: copts.Image, - Volumes: volumes, - MacAddress: copts.macAddress, - Entrypoint: entrypoint, - WorkingDir: copts.workingDir, - Labels: runconfigopts.ConvertKVStringsToMap(labels), - Healthcheck: healthConfig, - } - if flags.Changed("stop-signal") { - config.StopSignal = copts.stopSignal - } - if flags.Changed("stop-timeout") { - config.StopTimeout = &copts.stopTimeout - } - - hostConfig := &container.HostConfig{ - Binds: binds, - ContainerIDFile: copts.containerIDFile, - OomScoreAdj: copts.oomScoreAdj, - AutoRemove: copts.autoRemove, - Privileged: copts.privileged, - PortBindings: portBindings, - Links: copts.links.GetAll(), - PublishAllPorts: copts.publishAll, - // Make sure the dns fields are never nil. - // New containers don't ever have those fields nil, - // but pre created containers can still have those nil values. - // See https://github.com/docker/docker/pull/17779 - // for a more detailed explanation on why we don't want that. - DNS: copts.dns.GetAllOrEmpty(), - DNSSearch: copts.dnsSearch.GetAllOrEmpty(), - DNSOptions: copts.dnsOptions.GetAllOrEmpty(), - ExtraHosts: copts.extraHosts.GetAll(), - VolumesFrom: copts.volumesFrom.GetAll(), - NetworkMode: container.NetworkMode(copts.netMode), - IpcMode: ipcMode, - PidMode: pidMode, - UTSMode: utsMode, - UsernsMode: usernsMode, - CapAdd: strslice.StrSlice(copts.capAdd.GetAll()), - CapDrop: strslice.StrSlice(copts.capDrop.GetAll()), - GroupAdd: copts.groupAdd.GetAll(), - RestartPolicy: restartPolicy, - SecurityOpt: securityOpts, - StorageOpt: storageOpts, - ReadonlyRootfs: copts.readonlyRootfs, - LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts}, - VolumeDriver: copts.volumeDriver, - Isolation: container.Isolation(copts.isolation), - ShmSize: copts.shmSize.Value(), - Resources: resources, - Tmpfs: tmpfs, - Sysctls: copts.sysctls.GetAll(), - Runtime: copts.runtime, - Mounts: mounts, - } - - if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { - return nil, errors.Errorf("Conflicting options: --restart and --rm") - } - - // only set this value if the user provided the flag, else it should default to nil - if flags.Changed("init") { - hostConfig.Init = &copts.init - } - - // When allocating stdin in attached mode, close stdin at client disconnect - if config.OpenStdin && config.AttachStdin { - config.StdinOnce = true - } - - networkingConfig := &networktypes.NetworkingConfig{ - EndpointsConfig: make(map[string]*networktypes.EndpointSettings), - } - - if copts.ipv4Address != "" || copts.ipv6Address != "" || copts.linkLocalIPs.Len() > 0 { - epConfig := &networktypes.EndpointSettings{} - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - - epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{ - IPv4Address: copts.ipv4Address, - IPv6Address: copts.ipv6Address, - } - - if copts.linkLocalIPs.Len() > 0 { - epConfig.IPAMConfig.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) - copy(epConfig.IPAMConfig.LinkLocalIPs, copts.linkLocalIPs.GetAll()) - } - } - - if hostConfig.NetworkMode.IsUserDefined() && len(hostConfig.Links) > 0 { - epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] - if epConfig == nil { - epConfig = &networktypes.EndpointSettings{} - } - epConfig.Links = make([]string, len(hostConfig.Links)) - copy(epConfig.Links, hostConfig.Links) - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - } - - if copts.aliases.Len() > 0 { - epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] - if epConfig == nil { - epConfig = &networktypes.EndpointSettings{} - } - epConfig.Aliases = make([]string, copts.aliases.Len()) - copy(epConfig.Aliases, copts.aliases.GetAll()) - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - } - - return &containerConfig{ - Config: config, - HostConfig: hostConfig, - NetworkingConfig: networkingConfig, - }, nil -} - -func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { - loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts) - if loggingDriver == "none" && len(loggingOpts) > 0 { - return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver) - } - return loggingOptsMap, nil -} - -// takes a local seccomp daemon, reads the file contents for sending to the daemon -func parseSecurityOpts(securityOpts []string) ([]string, error) { - for key, opt := range securityOpts { - con := strings.SplitN(opt, "=", 2) - if len(con) == 1 && con[0] != "no-new-privileges" { - if strings.Contains(opt, ":") { - con = strings.SplitN(opt, ":", 2) - } else { - return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) - } - } - if con[0] == "seccomp" && con[1] != "unconfined" { - f, err := ioutil.ReadFile(con[1]) - if err != nil { - return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) - } - b := bytes.NewBuffer(nil) - if err := json.Compact(b, f); err != nil { - return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) - } - securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) - } - } - - return securityOpts, nil -} - -// parses storage options per container into a map -func parseStorageOpts(storageOpts []string) (map[string]string, error) { - m := make(map[string]string) - for _, option := range storageOpts { - if strings.Contains(option, "=") { - opt := strings.SplitN(option, "=", 2) - m[opt[0]] = opt[1] - } else { - return nil, errors.Errorf("invalid storage option") - } - } - return m, nil -} - -// parseDevice parses a device mapping string to a container.DeviceMapping struct -func parseDevice(device string) (container.DeviceMapping, error) { - src := "" - dst := "" - permissions := "rwm" - arr := strings.Split(device, ":") - switch len(arr) { - case 3: - permissions = arr[2] - fallthrough - case 2: - if validDeviceMode(arr[1]) { - permissions = arr[1] - } else { - dst = arr[1] - } - fallthrough - case 1: - src = arr[0] - default: - return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device) - } - - if dst == "" { - dst = src - } - - deviceMapping := container.DeviceMapping{ - PathOnHost: src, - PathInContainer: dst, - CgroupPermissions: permissions, - } - return deviceMapping, nil -} - -// validateDeviceCgroupRule validates a device cgroup rule string format -// It will make sure 'val' is in the form: -// 'type major:minor mode' -func validateDeviceCgroupRule(val string) (string, error) { - if deviceCgroupRuleRegexp.MatchString(val) { - return val, nil - } - - return val, errors.Errorf("invalid device cgroup format '%s'", val) -} - -// validDeviceMode checks if the mode for device is valid or not. -// Valid mode is a composition of r (read), w (write), and m (mknod). -func validDeviceMode(mode string) bool { - var legalDeviceMode = map[rune]bool{ - 'r': true, - 'w': true, - 'm': true, - } - if mode == "" { - return false - } - for _, c := range mode { - if !legalDeviceMode[c] { - return false - } - legalDeviceMode[c] = false - } - return true -} - -// validateDevice validates a path for devices -// It will make sure 'val' is in the form: -// [host-dir:]container-path[:mode] -// It also validates the device mode. -func validateDevice(val string) (string, error) { - return validatePath(val, validDeviceMode) -} - -func validatePath(val string, validator func(string) bool) (string, error) { - var containerPath string - var mode string - - if strings.Count(val, ":") > 2 { - return val, errors.Errorf("bad format for path: %s", val) - } - - split := strings.SplitN(val, ":", 3) - if split[0] == "" { - return val, errors.Errorf("bad format for path: %s", val) - } - switch len(split) { - case 1: - containerPath = split[0] - val = path.Clean(containerPath) - case 2: - if isValid := validator(split[1]); isValid { - containerPath = split[0] - mode = split[1] - val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode) - } else { - containerPath = split[1] - val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath)) - } - case 3: - containerPath = split[1] - mode = split[2] - if isValid := validator(split[2]); !isValid { - return val, errors.Errorf("bad mode specified: %s", mode) - } - val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) - } - - if !path.IsAbs(containerPath) { - return val, errors.Errorf("%s is not an absolute path", containerPath) - } - return val, nil -} - -// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon. -// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped). -// In Windows driver letter appears in two situations: -// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option) -// b. A string in the format like `\\?\C:\Windows\...` (UNC). -// Therefore, a driver letter can only follow either a `:` or `\\` -// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`. -func volumeSplitN(raw string, n int) []string { - var array []string - if len(raw) == 0 || raw[0] == ':' { - // invalid - return nil - } - // numberOfParts counts the number of parts separated by a separator colon - numberOfParts := 0 - // left represents the left-most cursor in raw, updated at every `:` character considered as a separator. - left := 0 - // right represents the right-most cursor in raw incremented with the loop. Note this - // starts at index 1 as index 0 is already handle above as a special case. - for right := 1; right < len(raw); right++ { - // stop parsing if reached maximum number of parts - if n >= 0 && numberOfParts >= n { - break - } - if raw[right] != ':' { - continue - } - potentialDriveLetter := raw[right-1] - if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') { - if right > 1 { - beforePotentialDriveLetter := raw[right-2] - // Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`) - if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' { - // e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`. - array = append(array, raw[left:right]) - left = right + 1 - numberOfParts++ - } - // else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing. - } - // if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing. - } else { - // if `:` is not preceded by a potential drive letter, then consider it as a delimiter. - array = append(array, raw[left:right]) - left = right + 1 - numberOfParts++ - } - } - // need to take care of the last part - if left < len(raw) { - if n >= 0 && numberOfParts >= n { - // if the maximum number of parts is reached, just append the rest to the last part - // left-1 is at the last `:` that needs to be included since not considered a separator. - array[n-1] += raw[left-1:] - } else { - array = append(array, raw[left:]) - } - } - return array -} - -// validateAttach validates that the specified string is a valid attach option. -func validateAttach(val string) (string, error) { - s := strings.ToLower(val) - for _, str := range []string{"stdin", "stdout", "stderr"} { - if s == str { - return s, nil - } - } - return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") -} diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go deleted file mode 100644 index 2d0049768a..0000000000 --- a/cli/command/container/opts_test.go +++ /dev/null @@ -1,870 +0,0 @@ -package container - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "runtime" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/runconfig" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/pflag" - "github.com/stretchr/testify/assert" -) - -func TestValidateAttach(t *testing.T) { - valid := []string{ - "stdin", - "stdout", - "stderr", - "STDIN", - "STDOUT", - "STDERR", - } - if _, err := validateAttach("invalid"); err == nil { - t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") - } - - for _, attach := range valid { - value, err := validateAttach(attach) - if err != nil { - t.Fatal(err) - } - if value != strings.ToLower(attach) { - t.Fatalf("Expected [%v], got [%v]", attach, value) - } - } -} - -func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { - flags := pflag.NewFlagSet("run", pflag.ContinueOnError) - flags.SetOutput(ioutil.Discard) - flags.Usage = nil - copts := addFlags(flags) - if err := flags.Parse(args); err != nil { - return nil, nil, nil, err - } - // TODO: fix tests to accept ContainerConfig - containerConfig, err := parse(flags, copts) - if err != nil { - return nil, nil, nil, err - } - return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err -} - -func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { - config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) - return config, hostConfig, err -} - -func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { - config, hostConfig, err := parsetest(t, args) - if err != nil { - t.Fatal(err) - } - return config, hostConfig -} - -func TestParseRunLinks(t *testing.T) { - if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { - t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) - } - if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { - t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) - } - if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { - t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) - } -} - -func TestParseRunAttach(t *testing.T) { - if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - - if _, _, err := parsetest(t, "-a"); err == nil { - t.Fatal("Error parsing attach flags, `-a` should be an error but is not") - } - if _, _, err := parsetest(t, "-a invalid"); err == nil { - t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not") - } - if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdin -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdout -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stderr -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-d --rm"); err == nil { - t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not") - } -} - -func TestParseRunVolumes(t *testing.T) { - - // A single volume - arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) - } else if _, exists := config.Volumes[arr[0]]; !exists { - t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) - } - - // Two volumes - arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) - } else if _, exists := config.Volumes[arr[0]]; !exists { - t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) - } else if _, exists := config.Volumes[arr[1]]; !exists { - t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) - } - - // A single bind-mount - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { - t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) - } - - // Two bind-mounts. - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - // Two bind-mounts, first read-only, second read-write. - // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - // Similar to previous test but with alternate modes which are only supported by Linux - if runtime.GOOS != "windows" { - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - } - - // One bind mount and one volume - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { - t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) - } else if _, exists := config.Volumes[arr[1]]; !exists { - t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) - } - - // Root to non-c: drive letter (Windows specific) - if runtime.GOOS == "windows" { - arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { - t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) - } - } - -} - -// setupPlatformVolume takes two arrays of volume specs - a Unix style -// spec and a Windows style spec. Depending on the platform being unit tested, -// it returns one of them, along with a volume string that would be passed -// on the docker CLI (e.g. -v /bar -v /foo). -func setupPlatformVolume(u []string, w []string) ([]string, string) { - var a []string - if runtime.GOOS == "windows" { - a = w - } else { - a = u - } - s := "" - for _, v := range a { - s = s + "-v " + v + " " - } - return a, s -} - -// check if (a == c && b == d) || (a == d && b == c) -// because maps are randomized -func compareRandomizedStrings(a, b, c, d string) error { - if a == c && b == d { - return nil - } - if a == d && b == c { - return nil - } - return errors.Errorf("strings don't match") -} - -// Simple parse with MacAddress validation -func TestParseWithMacAddress(t *testing.T) { - invalidMacAddress := "--mac-address=invalidMacAddress" - validMacAddress := "--mac-address=92:d0:c6:0a:29:33" - if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { - t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) - } - if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { - t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) - } -} - -func TestParseWithMemory(t *testing.T) { - invalidMemory := "--memory=invalid" - _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) - testutil.ErrorContains(t, err, invalidMemory) - - _, hostconfig := mustParse(t, "--memory=1G") - assert.Equal(t, int64(1073741824), hostconfig.Memory) -} - -func TestParseWithMemorySwap(t *testing.T) { - invalidMemory := "--memory-swap=invalid" - - _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) - testutil.ErrorContains(t, err, invalidMemory) - - _, hostconfig := mustParse(t, "--memory-swap=1G") - assert.Equal(t, int64(1073741824), hostconfig.MemorySwap) - - _, hostconfig = mustParse(t, "--memory-swap=-1") - assert.Equal(t, int64(-1), hostconfig.MemorySwap) -} - -func TestParseHostname(t *testing.T) { - validHostnames := map[string]string{ - "hostname": "hostname", - "host-name": "host-name", - "hostname123": "hostname123", - "123hostname": "123hostname", - "hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error", - } - hostnameWithDomain := "--hostname=hostname.domainname" - hostnameWithDomainTld := "--hostname=hostname.domainname.tld" - for hostname, expectedHostname := range validHostnames { - if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { - t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname) - } - } - if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" { - t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname) - } - if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" { - t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname) - } -} - -func TestParseWithExpose(t *testing.T) { - invalids := map[string]string{ - ":": "invalid port format for --expose: :", - "8080:9090": "invalid port format for --expose: 8080:9090", - "/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", - "/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", - "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, - } - valids := map[string][]nat.Port{ - "8080/tcp": {"8080/tcp"}, - "8080/udp": {"8080/udp"}, - "8080/ncp": {"8080/ncp"}, - "8080-8080/udp": {"8080/udp"}, - "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, - } - for expose, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { - t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) - } - } - for expose, exposedPorts := range valids { - config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.ExposedPorts) != len(exposedPorts) { - t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) - } - for _, port := range exposedPorts { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) - } - } - } - // Merge with actual published port - config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.ExposedPorts) != 2 { - t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) - } - ports := []nat.Port{"80/tcp", "81/tcp"} - for _, port := range ports { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) - } - } -} - -func TestParseDevice(t *testing.T) { - valids := map[string]container.DeviceMapping{ - "/dev/snd": { - PathOnHost: "/dev/snd", - PathInContainer: "/dev/snd", - CgroupPermissions: "rwm", - }, - "/dev/snd:rw": { - PathOnHost: "/dev/snd", - PathInContainer: "/dev/snd", - CgroupPermissions: "rw", - }, - "/dev/snd:/something": { - PathOnHost: "/dev/snd", - PathInContainer: "/something", - CgroupPermissions: "rwm", - }, - "/dev/snd:/something:rw": { - PathOnHost: "/dev/snd", - PathInContainer: "/something", - CgroupPermissions: "rw", - }, - } - for device, deviceMapping := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(hostconfig.Devices) != 1 { - t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices) - } - if hostconfig.Devices[0] != deviceMapping { - t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) - } - } - -} - -func TestParseModes(t *testing.T) { - // ipc ko - if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { - t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) - } - // ipc ok - _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.IpcMode.Valid() { - t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) - } - // pid ko - if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { - t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) - } - // pid ok - _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.PidMode.Valid() { - t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) - } - // uts ko - if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { - t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) - } - // uts ok - _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.UTSMode.Valid() { - t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) - } - // shm-size ko - expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'` - if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr { - t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err) - } - // shm-size ok - _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.ShmSize != 134217728 { - t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) - } -} - -func TestParseRestartPolicy(t *testing.T) { - invalids := map[string]string{ - "always:2:3": "invalid restart policy format", - "on-failure:invalid": "maximum retry count must be an integer", - } - valids := map[string]container.RestartPolicy{ - "": {}, - "always": { - Name: "always", - MaximumRetryCount: 0, - }, - "on-failure:1": { - Name: "on-failure", - MaximumRetryCount: 1, - }, - } - for restart, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { - t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) - } - } - for restart, expected := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.RestartPolicy != expected { - t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) - } - } -} - -func TestParseRestartPolicyAutoRemove(t *testing.T) { - expected := "Conflicting options: --restart and --rm" - _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) - if err == nil || err.Error() != expected { - t.Fatalf("Expected error %v, but got none", expected) - } -} - -func TestParseHealth(t *testing.T) { - checkOk := func(args ...string) *container.HealthConfig { - config, _, _, err := parseRun(args) - if err != nil { - t.Fatalf("%#v: %v", args, err) - } - return config.Healthcheck - } - checkError := func(expected string, args ...string) { - config, _, _, err := parseRun(args) - if err == nil { - t.Fatalf("Expected error, but got %#v", config) - } - if err.Error() != expected { - t.Fatalf("Expected %#v, got %#v", expected, err) - } - } - health := checkOk("--no-healthcheck", "img", "cmd") - if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { - t.Fatalf("--no-healthcheck failed: %#v", health) - } - - health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") - if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { - t.Fatalf("--health-cmd: got %#v", health.Test) - } - if health.Timeout != 0 { - t.Fatalf("--health-cmd: timeout = %s", health.Timeout) - } - - checkError("--no-healthcheck conflicts with --health-* options", - "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") - - health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd") - if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second { - t.Fatalf("--health-*: got %#v", health) - } -} - -func TestParseLoggingOpts(t *testing.T) { - // logging opts ko - if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { - t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) - } - // logging opts ok - _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { - t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) - } -} - -func TestParseEnvfileVariables(t *testing.T) { - e := "open nonexistent: no such file or directory" - if runtime.GOOS == "windows" { - e = "open nonexistent: The system cannot find the file specified." - } - // env ko - if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // env ok - config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { - t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) - } - config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { - t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) - } -} - -func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { - // UTF8 with BOM - config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} - if len(config.Env) != len(env) { - t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) - } - for i, v := range env { - if config.Env[i] != v { - t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) - } - } - - // UTF16 with BOM - e := "contains invalid utf8 bytes at line" - if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // UTF16BE with BOM - if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } -} - -func TestParseLabelfileVariables(t *testing.T) { - e := "open nonexistent: no such file or directory" - if runtime.GOOS == "windows" { - e = "open nonexistent: The system cannot find the file specified." - } - // label ko - if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // label ok - config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { - t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) - } - config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { - t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) - } -} - -func TestParseEntryPoint(t *testing.T) { - config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) - if err != nil { - t.Fatal(err) - } - if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { - t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) - } -} - -// This tests the cases for binds which are generated through -// DecodeContainerConfig rather than Parse() -func TestDecodeContainerConfigVolumes(t *testing.T) { - - // Root to root - bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // No destination path - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // // No destination path or mode - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A whole lot of nothing - bindsOrVols = []string{`:`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A whole lot of nothing with no mode - bindsOrVols = []string{`::`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Too much including an invalid mode - wTmp := os.Getenv("TEMP") - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Windows specific error tests - if runtime.GOOS == "windows" { - // Volume which does not include a drive letter - bindsOrVols = []string{`\tmp`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Root to C-Drive - bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Container path that does not include a drive letter - bindsOrVols = []string{`c:\windows:\somewhere`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - } - - // Linux-specific error tests - if runtime.GOOS != "windows" { - // Just root - bindsOrVols = []string{`/`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A single volume that looks like a bind mount passed in Volumes. - // This should be handled as a bind mount, not a volume. - vols := []string{`/foo:/bar`} - if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { - t.Fatal("Volume /foo:/bar should have succeeded as a volume name") - } else if hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) - } else if _, exists := config.Volumes[vols[0]]; !exists { - t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) - } - - } -} - -// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes -// to call DecodeContainerConfig. It effectively does what a client would -// do when calling the daemon by constructing a JSON stream of a -// ContainerConfigWrapper which is populated by the set of volume specs -// passed into it. It returns a config and a hostconfig which can be -// validated to ensure DecodeContainerConfig has manipulated the structures -// correctly. -func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) { - var ( - b []byte - err error - c *container.Config - h *container.HostConfig - ) - w := runconfig.ContainerConfigWrapper{ - Config: &container.Config{ - Volumes: map[string]struct{}{}, - }, - HostConfig: &container.HostConfig{ - NetworkMode: "none", - Binds: binds, - }, - } - for _, v := range volumes { - w.Config.Volumes[v] = struct{}{} - } - if b, err = json.Marshal(w); err != nil { - return nil, nil, errors.Errorf("Error on marshal %s", err.Error()) - } - c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) - if err != nil { - return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err) - } - if c == nil || h == nil { - return nil, nil, errors.Errorf("Empty config or hostconfig") - } - - return c, h, err -} - -func TestVolumeSplitN(t *testing.T) { - for _, x := range []struct { - input string - n int - expected []string - }{ - {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, - {`:C:\foo:d:`, -1, nil}, - {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, - {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, - {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, - - {`d:\`, -1, []string{`d:\`}}, - {`d:`, -1, []string{`d:`}}, - {`d:\path`, -1, []string{`d:\path`}}, - {`d:\path with space`, -1, []string{`d:\path with space`}}, - {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, - {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, - {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, - {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, - {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, - {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, - {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, - {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, - {`name:D:`, -1, []string{`name`, `D:`}}, - {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, - {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, - {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, - {`c:\Windows`, -1, []string{`c:\Windows`}}, - {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, - - {``, -1, nil}, - {`.`, -1, []string{`.`}}, - {`..\`, -1, []string{`..\`}}, - {`c:\:..\`, -1, []string{`c:\`, `..\`}}, - {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, - - // Cover directories with one-character name - {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, - } { - res := volumeSplitN(x.input, x.n) - if len(res) < len(x.expected) { - t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) - } - for i, e := range res { - if e != x.expected[i] { - t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) - } - } - } -} - -func TestValidateDevice(t *testing.T) { - valid := []string{ - "/home", - "/home:/home", - "/home:/something/else", - "/with space", - "/home:/with space", - "relative:/absolute-path", - "hostPath:/containerPath:r", - "/hostPath:/containerPath:rw", - "/hostPath:/containerPath:mrw", - } - invalid := map[string]string{ - "": "bad format for path: ", - "./": "./ is not an absolute path", - "../": "../ is not an absolute path", - "/:../": "../ is not an absolute path", - "/:path": "path is not an absolute path", - ":": "bad format for path: :", - "/tmp:": " is not an absolute path", - ":test": "bad format for path: :test", - ":/test": "bad format for path: :/test", - "tmp:": " is not an absolute path", - ":test:": "bad format for path: :test:", - "::": "bad format for path: ::", - ":::": "bad format for path: :::", - "/tmp:::": "bad format for path: /tmp:::", - ":/tmp::": "bad format for path: :/tmp::", - "path:ro": "ro is not an absolute path", - "path:rr": "rr is not an absolute path", - "a:/b:ro": "bad mode specified: ro", - "a:/b:rr": "bad mode specified: rr", - } - - for _, path := range valid { - if _, err := validateDevice(path); err != nil { - t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) - } - } - - for path, expectedError := range invalid { - if _, err := validateDevice(path); err == nil { - t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) - } else { - if err.Error() != expectedError { - t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) - } - } - } -} diff --git a/cli/command/container/pause.go b/cli/command/container/pause.go deleted file mode 100644 index 095a0db2c2..0000000000 --- a/cli/command/container/pause.go +++ /dev/null @@ -1,49 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pauseOptions struct { - containers []string -} - -// NewPauseCommand creates a new cobra.Command for `docker pause` -func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pauseOptions - - return &cobra.Command{ - Use: "pause CONTAINER [CONTAINER...]", - Short: "Pause all processes within one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runPause(dockerCli, &opts) - }, - } -} - -func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error { - ctx := context.Background() - - var errs []string - errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause) - for _, container := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/port.go b/cli/command/container/port.go deleted file mode 100644 index 2793f6bc6b..0000000000 --- a/cli/command/container/port.go +++ /dev/null @@ -1,78 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type portOptions struct { - container string - - port string -} - -// NewPortCommand creates a new cobra.Command for `docker port` -func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts portOptions - - cmd := &cobra.Command{ - Use: "port CONTAINER [PRIVATE_PORT[/PROTO]]", - Short: "List port mappings or a specific mapping for the container", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - if len(args) > 1 { - opts.port = args[1] - } - return runPort(dockerCli, &opts) - }, - } - return cmd -} - -func runPort(dockerCli *command.DockerCli, opts *portOptions) error { - ctx := context.Background() - - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if opts.port != "" { - port := opts.port - proto := "tcp" - parts := strings.SplitN(port, "/", 2) - - if len(parts) == 2 && len(parts[1]) != 0 { - port = parts[0] - proto = parts[1] - } - natPort := port + "/" + proto - newP, err := nat.NewPort(proto, port) - if err != nil { - return err - } - if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil { - for _, frontend := range frontends { - fmt.Fprintf(dockerCli.Out(), "%s:%s\n", frontend.HostIP, frontend.HostPort) - } - return nil - } - return errors.Errorf("Error: No public port '%s' published for %s", natPort, opts.container) - } - - for from, frontends := range c.NetworkSettings.Ports { - for _, frontend := range frontends { - fmt.Fprintf(dockerCli.Out(), "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort) - } - } - - return nil -} diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go deleted file mode 100644 index cf12dc71fe..0000000000 --- a/cli/command/container/prune.go +++ /dev/null @@ -1,78 +0,0 @@ -package container - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for containers -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all stopped containers", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const warning = `WARNING! This will remove all stopped containers. -Are you sure you want to continue?` - -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) - - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.ContainersDeleted) > 0 { - output = "Deleted Containers:\n" - for _, id := range report.ContainersDeleted { - output += id + "\n" - } - spaceReclaimed = report.SpaceReclaimed - } - - return -} - -// RunPrune calls the Container Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) -} diff --git a/cli/command/container/ps_test.go b/cli/command/container/ps_test.go deleted file mode 100644 index 47665b0e2c..0000000000 --- a/cli/command/container/ps_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/opts" - "github.com/stretchr/testify/assert" -) - -func TestBuildContainerListOptions(t *testing.T) { - filters := opts.NewFilterOpt() - assert.NoError(t, filters.Set("foo=bar")) - assert.NoError(t, filters.Set("baz=foo")) - - contexts := []struct { - psOpts *psOptions - expectedAll bool - expectedSize bool - expectedLimit int - expectedFilters map[string]string - }{ - { - psOpts: &psOptions{ - all: true, - size: true, - last: 5, - filter: filters, - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: true, - last: -1, - nLatest: true, - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 1, - expectedFilters: make(map[string]string), - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // With .Size, size should be true - format: "{{.Size}}", - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // With .Size, size should be true - format: "{{.Size}} {{.CreatedAt}} {{.Networks}}", - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // Without .Size, size should be false - format: "{{.CreatedAt}} {{.Networks}}", - }, - expectedAll: true, - expectedSize: false, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - } - - for _, c := range contexts { - options, err := buildContainerListOptions(c.psOpts) - assert.NoError(t, err) - - assert.Equal(t, c.expectedAll, options.All) - assert.Equal(t, c.expectedSize, options.Size) - assert.Equal(t, c.expectedLimit, options.Limit) - assert.Equal(t, len(c.expectedFilters), options.Filters.Len()) - - for k, v := range c.expectedFilters { - f := options.Filters - if !f.ExactMatch(k, v) { - t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k)) - } - } - } -} diff --git a/cli/command/container/rename.go b/cli/command/container/rename.go deleted file mode 100644 index 07b4852f47..0000000000 --- a/cli/command/container/rename.go +++ /dev/null @@ -1,51 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type renameOptions struct { - oldName string - newName string -} - -// NewRenameCommand creates a new cobra.Command for `docker rename` -func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts renameOptions - - cmd := &cobra.Command{ - Use: "rename CONTAINER NEW_NAME", - Short: "Rename a container", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.oldName = args[0] - opts.newName = args[1] - return runRename(dockerCli, &opts) - }, - } - return cmd -} - -func runRename(dockerCli *command.DockerCli, opts *renameOptions) error { - ctx := context.Background() - - oldName := strings.TrimSpace(opts.oldName) - newName := strings.TrimSpace(opts.newName) - - if oldName == "" || newName == "" { - return errors.New("Error: Neither old nor new names may be empty") - } - - if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { - fmt.Fprintln(dockerCli.Err(), err) - return errors.Errorf("Error: failed to rename container named %s", oldName) - } - return nil -} diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go deleted file mode 100644 index 73cd2507ee..0000000000 --- a/cli/command/container/restart.go +++ /dev/null @@ -1,62 +0,0 @@ -package container - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type restartOptions struct { - nSeconds int - nSecondsChanged bool - - containers []string -} - -// NewRestartCommand creates a new cobra.Command for `docker restart` -func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts restartOptions - - cmd := &cobra.Command{ - Use: "restart [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Restart one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.nSecondsChanged = cmd.Flags().Changed("time") - return runRestart(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVarP(&opts.nSeconds, "time", "t", 10, "Seconds to wait for stop before killing the container") - return cmd -} - -func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error { - ctx := context.Background() - var errs []string - var timeout *time.Duration - if opts.nSecondsChanged { - timeoutValue := time.Duration(opts.nSeconds) * time.Second - timeout = &timeoutValue - } - - for _, name := range opts.containers { - if err := dockerCli.Client().ContainerRestart(ctx, name, timeout); err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go deleted file mode 100644 index 887b5c5d34..0000000000 --- a/cli/command/container/rm.go +++ /dev/null @@ -1,73 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type rmOptions struct { - rmVolumes bool - rmLink bool - force bool - - containers []string -} - -// NewRmCommand creates a new cobra.Command for `docker rm` -func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts rmOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Remove one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runRm(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove the volumes associated with the container") - flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link") - flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)") - return cmd -} - -func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { - ctx := context.Background() - - var errs []string - options := types.ContainerRemoveOptions{ - RemoveVolumes: opts.rmVolumes, - RemoveLinks: opts.rmLink, - Force: opts.force, - } - - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - container = strings.Trim(container, "/") - if container == "" { - return errors.New("Container name cannot be empty") - } - return dockerCli.Client().ContainerRemove(ctx, container, options) - }) - - for _, name := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/run.go b/cli/command/container/run.go deleted file mode 100644 index bab6a9cf13..0000000000 --- a/cli/command/container/run.go +++ /dev/null @@ -1,296 +0,0 @@ -package container - -import ( - "fmt" - "io" - "net/http/httputil" - "os" - "runtime" - "strings" - "syscall" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/promise" - "github.com/docker/docker/pkg/signal" - "github.com/docker/libnetwork/resolvconf/dns" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type runOptions struct { - detach bool - sigProxy bool - name string - detachKeys string -} - -// NewRunCommand create a new `docker run` command -func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts runOptions - var copts *containerOptions - - cmd := &cobra.Command{ - Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Run a command in a new container", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - copts.Image = args[0] - if len(args) > 1 { - copts.Args = args[1:] - } - return runRun(dockerCli, cmd.Flags(), &opts, copts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - // These are flags not stored in Config/HostConfig - flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID") - flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process") - flags.StringVar(&opts.name, "name", "", "Assign a name to the container") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - - // Add an explicit help that doesn't have a `-h` to prevent the conflict - // with hostname - flags.Bool("help", false, "Print usage") - - command.AddTrustVerificationFlags(flags) - copts = addFlags(flags) - return cmd -} - -func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) { - if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { - fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.") - } -} - -// check the DNS settings passed via --dns against localhost regexp to warn if -// they are trying to set a DNS to a localhost address -func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) { - for _, dnsIP := range hostConfig.DNS { - if dns.IsLocalhost(dnsIP) { - fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) - return - } - } -} - -func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error { - containerConfig, err := parse(flags, copts) - // just in case the parse does not exit - if err != nil { - reportError(dockerCli.Err(), "run", err.Error(), true) - return cli.StatusError{StatusCode: 125} - } - return runContainer(dockerCli, opts, copts, containerConfig) -} - -func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error { - config := containerConfig.Config - hostConfig := containerConfig.HostConfig - stdout, stderr := dockerCli.Out(), dockerCli.Err() - client := dockerCli.Client() - - // TODO: pass this as an argument - cmdPath := "run" - - warnOnOomKillDisable(*hostConfig, stderr) - warnOnLocalhostDNS(*hostConfig, stderr) - - config.ArgsEscaped = false - - if !opts.detach { - if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil { - return err - } - } else { - if copts.attach.Len() != 0 { - return errors.New("Conflicting options: -a and -d") - } - - config.AttachStdin = false - config.AttachStdout = false - config.AttachStderr = false - config.StdinOnce = false - } - - // Disable sigProxy when in TTY mode - if config.Tty { - opts.sigProxy = false - } - - // Telling the Windows daemon the initial size of the tty during start makes - // a far better user experience rather than relying on subsequent resizes - // to cause things to catch up. - if runtime.GOOS == "windows" { - hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() - } - - ctx, cancelFun := context.WithCancel(context.Background()) - - createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name) - if err != nil { - reportError(stderr, cmdPath, err.Error(), true) - return runStartContainerErr(err) - } - if opts.sigProxy { - sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID) - defer signal.StopCatch(sigc) - } - var ( - waitDisplayID chan struct{} - errCh chan error - ) - if !config.AttachStdout && !config.AttachStderr { - // Make this asynchronous to allow the client to write to stdin before having to read the ID - waitDisplayID = make(chan struct{}) - go func() { - defer close(waitDisplayID) - fmt.Fprintln(stdout, createResponse.ID) - }() - } - attach := config.AttachStdin || config.AttachStdout || config.AttachStderr - if attach { - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID) - defer close() - if err != nil { - return err - } - } - - statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove) - - //start the container - if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { - // If we have holdHijackedConnection, we should notify - // holdHijackedConnection we are going to exit and wait - // to avoid the terminal are not restored. - if attach { - cancelFun() - <-errCh - } - - reportError(stderr, cmdPath, err.Error(), false) - if copts.autoRemove { - // wait container to be removed - <-statusChan - } - return runStartContainerErr(err) - } - - if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil { - fmt.Fprintln(stderr, "Error monitoring TTY size:", err) - } - } - - if errCh != nil { - if err := <-errCh; err != nil { - logrus.Debugf("Error hijack: %s", err) - return err - } - } - - // Detached mode: wait for the id to be displayed and return. - if !config.AttachStdout && !config.AttachStderr { - // Detached mode - <-waitDisplayID - return nil - } - - status := <-statusChan - if status != 0 { - return cli.StatusError{StatusCode: status} - } - return nil -} - -func attachContainer( - ctx context.Context, - dockerCli *command.DockerCli, - errCh *chan error, - config *container.Config, - containerID string, -) (func(), error) { - stdout, stderr := dockerCli.Out(), dockerCli.Err() - var ( - out, cerr io.Writer - in io.ReadCloser - ) - if config.AttachStdin { - in = dockerCli.In() - } - if config.AttachStdout { - out = stdout - } - if config.AttachStderr { - if config.Tty { - cerr = stdout - } else { - cerr = stderr - } - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: config.AttachStdin, - Stdout: config.AttachStdout, - Stderr: config.AttachStderr, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach returns an ErrPersistEOF (connection closed) - // means server met an error and put it in Hijacked connection - // keep the error and read detailed error message from hijacked connection later - return nil, errAttach - } - - *errCh = promise.Go(func() error { - if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil { - return errHijack - } - return errAttach - }) - return resp.Close, nil -} - -// reportError is a utility method that prints a user-friendly message -// containing the error that occurred during parsing and a suggestion to get help -func reportError(stderr io.Writer, name string, str string, withHelp bool) { - str = strings.TrimSuffix(str, ".") + "." - if withHelp { - str += "\nSee '" + os.Args[0] + " " + name + " --help'." - } - fmt.Fprintf(stderr, "%s: %s\n", os.Args[0], str) -} - -// if container start fails with 'not found'/'no such' error, return 127 -// if container start fails with 'permission denied' error, return 126 -// return 125 for generic docker daemon failures -func runStartContainerErr(err error) error { - trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ") - statusError := cli.StatusError{StatusCode: 125} - if strings.Contains(trimmedErr, "executable file not found") || - strings.Contains(trimmedErr, "no such file or directory") || - strings.Contains(trimmedErr, "system cannot find the file specified") { - statusError = cli.StatusError{StatusCode: 127} - } else if strings.Contains(trimmedErr, syscall.EACCES.Error()) { - statusError = cli.StatusError{StatusCode: 126} - } - - return statusError -} diff --git a/cli/command/container/start.go b/cli/command/container/start.go deleted file mode 100644 index 7702cd4a75..0000000000 --- a/cli/command/container/start.go +++ /dev/null @@ -1,179 +0,0 @@ -package container - -import ( - "fmt" - "io" - "net/http/httputil" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/promise" - "github.com/docker/docker/pkg/signal" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type startOptions struct { - attach bool - openStdin bool - detachKeys string - checkpoint string - checkpointDir string - - containers []string -} - -// NewStartCommand creates a new cobra.Command for `docker start` -func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts startOptions - - cmd := &cobra.Command{ - Use: "start [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Start one or more stopped containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runStart(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") - flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - - flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") - flags.SetAnnotation("checkpoint", "experimental", nil) - flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") - flags.SetAnnotation("checkpoint-dir", "experimental", nil) - return cmd -} - -func runStart(dockerCli *command.DockerCli, opts *startOptions) error { - ctx, cancelFun := context.WithCancel(context.Background()) - - if opts.attach || opts.openStdin { - // We're going to attach to a container. - // 1. Ensure we only have one container. - if len(opts.containers) > 1 { - return errors.New("You cannot start and attach multiple containers at once.") - } - - // 2. Attach to the container. - container := opts.containers[0] - c, err := dockerCli.Client().ContainerInspect(ctx, container) - if err != nil { - return err - } - - // We always use c.ID instead of container to maintain consistency during `docker start` - if !c.Config.Tty { - sigc := ForwardAllSignals(ctx, dockerCli, c.ID) - defer signal.StopCatch(sigc) - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: opts.openStdin && c.Config.OpenStdin, - Stdout: true, - Stderr: true, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - var in io.ReadCloser - - if options.Stdin { - in = dockerCli.In() - } - - resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach return an ErrPersistEOF (connection closed) - // means server met an error and already put it in Hijacked connection, - // we would keep the error and read the detailed error message from hijacked connection - return errAttach - } - defer resp.Close() - cErr := promise.Go(func() error { - errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp) - if errHijack == nil { - return errAttach - } - return errHijack - }) - - // 3. We should open a channel for receiving status code of the container - // no matter it's detached, removed on daemon side(--rm) or exit normally. - statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) - startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - } - - // 4. Start the container. - if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil { - cancelFun() - <-cErr - if c.HostConfig.AutoRemove { - // wait container to be removed - <-statusChan - } - return err - } - - // 5. Wait for attachment to break. - if c.Config.Tty && dockerCli.Out().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil { - fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) - } - } - if attchErr := <-cErr; attchErr != nil { - return attchErr - } - - if status := <-statusChan; status != 0 { - return cli.StatusError{StatusCode: status} - } - } else if opts.checkpoint != "" { - if len(opts.containers) > 1 { - return errors.New("You cannot restore multiple containers at once.") - } - container := opts.containers[0] - startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - } - return dockerCli.Client().ContainerStart(ctx, container, startOptions) - - } else { - // We're not going to attach to anything. - // Start as many containers as we want. - return startContainersWithoutAttachments(ctx, dockerCli, opts.containers) - } - - return nil -} - -func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.DockerCli, containers []string) error { - var failedContainers []string - for _, container := range containers { - if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { - fmt.Fprintln(dockerCli.Err(), err) - failedContainers = append(failedContainers, container) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - - if len(failedContainers) > 0 { - return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) - } - return nil -} diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go deleted file mode 100644 index c420e8151e..0000000000 --- a/cli/command/container/stats.go +++ /dev/null @@ -1,242 +0,0 @@ -package container - -import ( - "fmt" - "io" - "strings" - "sync" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type statsOptions struct { - all bool - noStream bool - format string - containers []string -} - -// NewStatsCommand creates a new cobra.Command for `docker stats` -func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts statsOptions - - cmd := &cobra.Command{ - Use: "stats [OPTIONS] [CONTAINER...]", - Short: "Display a live stream of container(s) resource usage statistics", - Args: cli.RequiresMinArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runStats(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") - flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - return cmd -} - -// runStats displays a live stream of resource usage statistics for one or more containers. -// This shows real-time information on CPU usage, memory usage, and network I/O. -func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { - showAll := len(opts.containers) == 0 - closeChan := make(chan error) - - ctx := context.Background() - - // monitorContainerEvents watches for container creation and removal (only - // used when calling `docker stats` without arguments). - monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) { - f := filters.NewArgs() - f.Add("type", "container") - options := types.EventsOptions{ - Filters: f, - } - - eventq, errq := dockerCli.Client().Events(ctx, options) - - // Whether we successfully subscribed to eventq or not, we can now - // unblock the main goroutine. - close(started) - - for { - select { - case event := <-eventq: - c <- event - case err := <-errq: - closeChan <- err - return - } - } - } - - // Get the daemonOSType if not set already - if daemonOSType == "" { - svctx := context.Background() - sv, err := dockerCli.Client().ServerVersion(svctx) - if err != nil { - return err - } - daemonOSType = sv.Os - } - - // waitFirst is a WaitGroup to wait first stat data's reach for each container - waitFirst := &sync.WaitGroup{} - - cStats := stats{} - // getContainerList simulates creation event for all previously existing - // containers (only used when calling `docker stats` without arguments). - getContainerList := func() { - options := types.ContainerListOptions{ - All: opts.all, - } - cs, err := dockerCli.Client().ContainerList(ctx, options) - if err != nil { - closeChan <- err - } - for _, container := range cs { - s := formatter.NewContainerStats(container.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - } - - if showAll { - // If no names were specified, start a long running goroutine which - // monitors container events. We make sure we're subscribed before - // retrieving the list of running containers to avoid a race where we - // would "miss" a creation. - started := make(chan struct{}) - eh := command.InitEventHandler() - eh.Handle("create", func(e events.Message) { - if opts.all { - s := formatter.NewContainerStats(e.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - }) - - eh.Handle("start", func(e events.Message) { - s := formatter.NewContainerStats(e.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - }) - - eh.Handle("die", func(e events.Message) { - if !opts.all { - cStats.remove(e.ID[:12]) - } - }) - - eventChan := make(chan events.Message) - go eh.Watch(eventChan) - go monitorContainerEvents(started, eventChan) - defer close(eventChan) - <-started - - // Start a short-lived goroutine to retrieve the initial list of - // containers. - getContainerList() - } else { - // Artificially send creation events for the containers we were asked to - // monitor (same code path than we use when monitoring all containers). - for _, name := range opts.containers { - s := formatter.NewContainerStats(name, daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - - // We don't expect any asynchronous errors: closeChan can be closed. - close(closeChan) - - // Do a quick pause to detect any error with the provided list of - // container names. - time.Sleep(1500 * time.Millisecond) - var errs []string - cStats.mu.Lock() - for _, c := range cStats.cs { - if err := c.GetError(); err != nil { - errs = append(errs, err.Error()) - } - } - cStats.mu.Unlock() - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - } - - // before print to screen, make sure each container get at least one valid stat data - waitFirst.Wait() - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().StatsFormat) > 0 { - format = dockerCli.ConfigFile().StatsFormat - } else { - format = formatter.TableFormatKey - } - } - statsCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewStatsFormat(format, daemonOSType), - } - cleanScreen := func() { - if !opts.noStream { - fmt.Fprint(dockerCli.Out(), "\033[2J") - fmt.Fprint(dockerCli.Out(), "\033[H") - } - } - - var err error - for range time.Tick(500 * time.Millisecond) { - cleanScreen() - ccstats := []formatter.StatsEntry{} - cStats.mu.Lock() - for _, c := range cStats.cs { - ccstats = append(ccstats, c.GetStatistics()) - } - cStats.mu.Unlock() - if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil { - break - } - if len(cStats.cs) == 0 && !showAll { - break - } - if opts.noStream { - break - } - select { - case err, ok := <-closeChan: - if ok { - if err != nil { - // this is suppressing "unexpected EOF" in the cli when the - // daemon restarts so it shutdowns cleanly - if err == io.ErrUnexpectedEOF { - return nil - } - return err - } - } - default: - // just skip - } - } - return err -} diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go deleted file mode 100644 index 5cbcf03e40..0000000000 --- a/cli/command/container/stats_helpers.go +++ /dev/null @@ -1,229 +0,0 @@ -package container - -import ( - "encoding/json" - "io" - "strings" - "sync" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type stats struct { - ostype string - mu sync.Mutex - cs []*formatter.ContainerStats -} - -// daemonOSType is set once we have at least one stat for a container -// from the daemon. It is used to ensure we print the right header based -// on the daemon platform. -var daemonOSType string - -func (s *stats) add(cs *formatter.ContainerStats) bool { - s.mu.Lock() - defer s.mu.Unlock() - if _, exists := s.isKnownContainer(cs.Container); !exists { - s.cs = append(s.cs, cs) - return true - } - return false -} - -func (s *stats) remove(id string) { - s.mu.Lock() - if i, exists := s.isKnownContainer(id); exists { - s.cs = append(s.cs[:i], s.cs[i+1:]...) - } - s.mu.Unlock() -} - -func (s *stats) isKnownContainer(cid string) (int, bool) { - for i, c := range s.cs { - if c.Container == cid { - return i, true - } - } - return -1, false -} - -func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { - logrus.Debugf("collecting stats for %s", s.Container) - var ( - getFirst bool - previousCPU uint64 - previousSystem uint64 - u = make(chan error, 1) - ) - - defer func() { - // if error happens and we get nothing of stats, release wait group whatever - if !getFirst { - getFirst = true - waitFirst.Done() - } - }() - - response, err := cli.ContainerStats(ctx, s.Container, streamStats) - if err != nil { - s.SetError(err) - return - } - defer response.Body.Close() - - dec := json.NewDecoder(response.Body) - go func() { - for { - var ( - v *types.StatsJSON - memPercent, cpuPercent float64 - blkRead, blkWrite uint64 // Only used on Linux - mem, memLimit, memPerc float64 - pidsStatsCurrent uint64 - ) - - if err := dec.Decode(&v); err != nil { - dec = json.NewDecoder(io.MultiReader(dec.Buffered(), response.Body)) - u <- err - if err == io.EOF { - break - } - time.Sleep(100 * time.Millisecond) - continue - } - - daemonOSType = response.OSType - - if daemonOSType != "windows" { - // MemoryStats.Limit will never be 0 unless the container is not running and we haven't - // got any data from cgroup - if v.MemoryStats.Limit != 0 { - memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 - } - previousCPU = v.PreCPUStats.CPUUsage.TotalUsage - previousSystem = v.PreCPUStats.SystemUsage - cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) - blkRead, blkWrite = calculateBlockIO(v.BlkioStats) - mem = float64(v.MemoryStats.Usage) - memLimit = float64(v.MemoryStats.Limit) - memPerc = memPercent - pidsStatsCurrent = v.PidsStats.Current - } else { - cpuPercent = calculateCPUPercentWindows(v) - blkRead = v.StorageStats.ReadSizeBytes - blkWrite = v.StorageStats.WriteSizeBytes - mem = float64(v.MemoryStats.PrivateWorkingSet) - } - netRx, netTx := calculateNetwork(v.Networks) - s.SetStatistics(formatter.StatsEntry{ - Name: v.Name, - ID: v.ID, - CPUPercentage: cpuPercent, - Memory: mem, - MemoryPercentage: memPerc, - MemoryLimit: memLimit, - NetworkRx: netRx, - NetworkTx: netTx, - BlockRead: float64(blkRead), - BlockWrite: float64(blkWrite), - PidsCurrent: pidsStatsCurrent, - }) - u <- nil - if !streamStats { - return - } - } - }() - for { - select { - case <-time.After(2 * time.Second): - // zero out the values if we have not received an update within - // the specified duration. - s.SetErrorAndReset(errors.New("timeout waiting for stats")) - // if this is the first stat you get, release WaitGroup - if !getFirst { - getFirst = true - waitFirst.Done() - } - case err := <-u: - s.SetError(err) - if err == io.EOF { - break - } - if err != nil { - continue - } - // if this is the first stat you get, release WaitGroup - if !getFirst { - getFirst = true - waitFirst.Done() - } - } - if !streamStats { - return - } - } -} - -func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { - var ( - cpuPercent = 0.0 - // calculate the change for the cpu usage of the container in between readings - cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) - // calculate the change for the entire system between readings - systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) - onlineCPUs = float64(v.CPUStats.OnlineCPUs) - ) - - if onlineCPUs == 0.0 { - onlineCPUs = float64(len(v.CPUStats.CPUUsage.PercpuUsage)) - } - if systemDelta > 0.0 && cpuDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 - } - return cpuPercent -} - -func calculateCPUPercentWindows(v *types.StatsJSON) float64 { - // Max number of 100ns intervals between the previous time read and now - possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals - possIntervals /= 100 // Convert to number of 100ns intervals - possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors - - // Intervals used - intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage - - // Percentage avoiding divide-by-zero - if possIntervals > 0 { - return float64(intervalsUsed) / float64(possIntervals) * 100.0 - } - return 0.00 -} - -func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) { - for _, bioEntry := range blkio.IoServiceBytesRecursive { - switch strings.ToLower(bioEntry.Op) { - case "read": - blkRead = blkRead + bioEntry.Value - case "write": - blkWrite = blkWrite + bioEntry.Value - } - } - return -} - -func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { - var rx, tx float64 - - for _, v := range network { - rx += float64(v.RxBytes) - tx += float64(v.TxBytes) - } - return rx, tx -} diff --git a/cli/command/container/stats_unit_test.go b/cli/command/container/stats_unit_test.go deleted file mode 100644 index 612914c9cd..0000000000 --- a/cli/command/container/stats_unit_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/api/types" -) - -func TestCalculateBlockIO(t *testing.T) { - blkio := types.BlkioStats{ - IoServiceBytesRecursive: []types.BlkioStatEntry{{Major: 8, Minor: 0, Op: "read", Value: 1234}, {Major: 8, Minor: 1, Op: "read", Value: 4567}, {Major: 8, Minor: 0, Op: "write", Value: 123}, {Major: 8, Minor: 1, Op: "write", Value: 456}}, - } - blkRead, blkWrite := calculateBlockIO(blkio) - if blkRead != 5801 { - t.Fatalf("blkRead = %d, want 5801", blkRead) - } - if blkWrite != 579 { - t.Fatalf("blkWrite = %d, want 579", blkWrite) - } -} diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go deleted file mode 100644 index 32729e1eae..0000000000 --- a/cli/command/container/stop.go +++ /dev/null @@ -1,67 +0,0 @@ -package container - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type stopOptions struct { - time int - timeChanged bool - - containers []string -} - -// NewStopCommand creates a new cobra.Command for `docker stop` -func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts stopOptions - - cmd := &cobra.Command{ - Use: "stop [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Stop one or more running containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.timeChanged = cmd.Flags().Changed("time") - return runStop(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVarP(&opts.time, "time", "t", 10, "Seconds to wait for stop before killing it") - return cmd -} - -func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { - ctx := context.Background() - - var timeout *time.Duration - if opts.timeChanged { - timeoutValue := time.Duration(opts.time) * time.Second - timeout = &timeoutValue - } - - var errs []string - - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error { - return dockerCli.Client().ContainerStop(ctx, id, timeout) - }) - for _, container := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/testdata/utf16.env b/cli/command/container/testdata/utf16.env deleted file mode 100755 index 3a73358fffbc0d5d3d4df985ccf2f4a1a29cdb2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 ucmezW&yB$!2yGdh7#tab7 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/update.go b/cli/command/container/update.go deleted file mode 100644 index a650815e8e..0000000000 --- a/cli/command/container/update.go +++ /dev/null @@ -1,134 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - containertypes "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type updateOptions struct { - blkioWeight uint16 - cpuPeriod int64 - cpuQuota int64 - cpuRealtimePeriod int64 - cpuRealtimeRuntime int64 - cpusetCpus string - cpusetMems string - cpuShares int64 - memory opts.MemBytes - memoryReservation opts.MemBytes - memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes - restartPolicy string - cpus opts.NanoCPUs - - nFlag int - - containers []string -} - -// NewUpdateCommand creates a new cobra.Command for `docker update` -func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts updateOptions - - cmd := &cobra.Command{ - Use: "update [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Update configuration of one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.nFlag = cmd.Flags().NFlag() - return runUpdate(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") - flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") - flags.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") - flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) - flags.Int64Var(&opts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds") - flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) - flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.VarP(&opts.memory, "memory", "m", "Memory limit") - flags.Var(&opts.memoryReservation, "memory-reservation", "Memory soft limit") - flags.Var(&opts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Var(&opts.kernelMemory, "kernel-memory", "Kernel memory limit") - flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits") - - flags.Var(&opts.cpus, "cpus", "Number of CPUs") - flags.SetAnnotation("cpus", "version", []string{"1.29"}) - - return cmd -} - -func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { - var err error - - if opts.nFlag == 0 { - return errors.New("You must provide one or more flags when using this command.") - } - - var restartPolicy containertypes.RestartPolicy - if opts.restartPolicy != "" { - restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy) - if err != nil { - return err - } - } - - resources := containertypes.Resources{ - BlkioWeight: opts.blkioWeight, - CpusetCpus: opts.cpusetCpus, - CpusetMems: opts.cpusetMems, - CPUShares: opts.cpuShares, - Memory: opts.memory.Value(), - MemoryReservation: opts.memoryReservation.Value(), - MemorySwap: opts.memorySwap.Value(), - KernelMemory: opts.kernelMemory.Value(), - CPUPeriod: opts.cpuPeriod, - CPUQuota: opts.cpuQuota, - CPURealtimePeriod: opts.cpuRealtimePeriod, - CPURealtimeRuntime: opts.cpuRealtimeRuntime, - NanoCPUs: opts.cpus.Value(), - } - - updateConfig := containertypes.UpdateConfig{ - Resources: resources, - RestartPolicy: restartPolicy, - } - - ctx := context.Background() - - var ( - warns []string - errs []string - ) - for _, container := range opts.containers { - r, err := dockerCli.Client().ContainerUpdate(ctx, container, updateConfig) - if err != nil { - errs = append(errs, err.Error()) - } else { - fmt.Fprintln(dockerCli.Out(), container) - } - warns = append(warns, r.Warnings...) - } - if len(warns) > 0 { - fmt.Fprintln(dockerCli.Out(), strings.Join(warns, "\n")) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/utils.go b/cli/command/container/utils.go deleted file mode 100644 index e4664b745c..0000000000 --- a/cli/command/container/utils.go +++ /dev/null @@ -1,142 +0,0 @@ -package container - -import ( - "strconv" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/cli/command" - clientapi "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) chan int { - if len(containerID) == 0 { - // containerID can never be empty - panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") - } - - var removeErr error - statusChan := make(chan int) - exitCode := 125 - - // Get events via Events API - f := filters.NewArgs() - f.Add("type", "container") - f.Add("container", containerID) - options := types.EventsOptions{ - Filters: f, - } - eventCtx, cancel := context.WithCancel(ctx) - eventq, errq := dockerCli.Client().Events(eventCtx, options) - - eventProcessor := func(e events.Message) bool { - stopProcessing := false - switch e.Status { - case "die": - if v, ok := e.Actor.Attributes["exitCode"]; ok { - code, cerr := strconv.Atoi(v) - if cerr != nil { - logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr) - } else { - exitCode = code - } - } - if !waitRemove { - stopProcessing = true - } else { - // If we are talking to an older daemon, `AutoRemove` is not supported. - // We need to fall back to the old behavior, which is client-side removal - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.25") { - go func() { - removeErr = dockerCli.Client().ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true}) - if removeErr != nil { - logrus.Errorf("error removing container: %v", removeErr) - cancel() // cancel the event Q - } - }() - } - } - case "detach": - exitCode = 0 - stopProcessing = true - case "destroy": - stopProcessing = true - } - return stopProcessing - } - - go func() { - defer func() { - statusChan <- exitCode // must always send an exit code or the caller will block - cancel() - }() - - for { - select { - case <-eventCtx.Done(): - if removeErr != nil { - return - } - case evt := <-eventq: - if eventProcessor(evt) { - return - } - case err := <-errq: - logrus.Errorf("error getting events from daemon: %v", err) - return - } - } - }() - - return statusChan -} - -// getExitCode performs an inspect on the container. It returns -// the running state and the exit code. -func getExitCode(ctx context.Context, dockerCli *command.DockerCli, containerID string) (bool, int, error) { - c, err := dockerCli.Client().ContainerInspect(ctx, containerID) - if err != nil { - // If we can't connect, then the daemon probably died. - if !clientapi.IsErrConnectionFailed(err) { - return false, -1, err - } - return false, -1, nil - } - return c.State.Running, c.State.ExitCode, nil -} - -func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error { - if len(containers) == 0 { - return nil - } - const defaultParallel int = 50 - sem := make(chan struct{}, defaultParallel) - errChan := make(chan error) - - // make sure result is printed in correct order - output := map[string]chan error{} - for _, c := range containers { - output[c] = make(chan error, 1) - } - go func() { - for _, c := range containers { - err := <-output[c] - errChan <- err - } - }() - - go func() { - for _, c := range containers { - sem <- struct{}{} // Wait for active queue sem to drain. - go func(container string) { - output[container] <- op(ctx, container) - <-sem - }(c) - } - }() - return errChan -} diff --git a/cli/command/container/wait.go b/cli/command/container/wait.go deleted file mode 100644 index f978207b94..0000000000 --- a/cli/command/container/wait.go +++ /dev/null @@ -1,50 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type waitOptions struct { - containers []string -} - -// NewWaitCommand creates a new cobra.Command for `docker wait` -func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts waitOptions - - cmd := &cobra.Command{ - Use: "wait CONTAINER [CONTAINER...]", - Short: "Block until one or more containers stop, then print their exit codes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runWait(dockerCli, &opts) - }, - } - return cmd -} - -func runWait(dockerCli *command.DockerCli, opts *waitOptions) error { - ctx := context.Background() - - var errs []string - for _, container := range opts.containers { - status, err := dockerCli.Client().ContainerWait(ctx, container) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%d\n", status) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/events_utils.go b/cli/command/events_utils.go deleted file mode 100644 index e710c97576..0000000000 --- a/cli/command/events_utils.go +++ /dev/null @@ -1,49 +0,0 @@ -package command - -import ( - "sync" - - "github.com/Sirupsen/logrus" - eventtypes "github.com/docker/docker/api/types/events" -) - -type eventProcessor func(eventtypes.Message, error) error - -// EventHandler is abstract interface for user to customize -// own handle functions of each type of events -type EventHandler interface { - Handle(action string, h func(eventtypes.Message)) - Watch(c <-chan eventtypes.Message) -} - -// InitEventHandler initializes and returns an EventHandler -func InitEventHandler() EventHandler { - return &eventHandler{handlers: make(map[string]func(eventtypes.Message))} -} - -type eventHandler struct { - handlers map[string]func(eventtypes.Message) - mu sync.Mutex -} - -func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { - w.mu.Lock() - w.handlers[action] = h - w.mu.Unlock() -} - -// Watch ranges over the passed in event chan and processes the events based on the -// handlers created for a given action. -// To stop watching, close the event chan. -func (w *eventHandler) Watch(c <-chan eventtypes.Message) { - for e := range c { - w.mu.Lock() - h, exists := w.handlers[e.Action] - w.mu.Unlock() - if !exists { - continue - } - logrus.Debugf("event handler: received event: %v", e) - go h(e) - } -} diff --git a/cli/command/formatter/checkpoint.go b/cli/command/formatter/checkpoint.go deleted file mode 100644 index 041fcafb7d..0000000000 --- a/cli/command/formatter/checkpoint.go +++ /dev/null @@ -1,52 +0,0 @@ -package formatter - -import "github.com/docker/docker/api/types" - -const ( - defaultCheckpointFormat = "table {{.Name}}" - - checkpointNameHeader = "CHECKPOINT NAME" -) - -// NewCheckpointFormat returns a format for use with a checkpoint Context -func NewCheckpointFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultCheckpointFormat - } - return Format(source) -} - -// CheckpointWrite writes formatted checkpoints using the Context -func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error { - render := func(format func(subContext subContext) error) error { - for _, checkpoint := range checkpoints { - if err := format(&checkpointContext{c: checkpoint}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newCheckpointContext(), render) -} - -type checkpointContext struct { - HeaderContext - c types.Checkpoint -} - -func newCheckpointContext() *checkpointContext { - cpCtx := checkpointContext{} - cpCtx.header = volumeHeaderContext{ - "Name": checkpointNameHeader, - } - return &cpCtx -} - -func (c *checkpointContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *checkpointContext) Name() string { - return c.c.Name -} diff --git a/cli/command/formatter/checkpoint_test.go b/cli/command/formatter/checkpoint_test.go deleted file mode 100644 index e88c4d0132..0000000000 --- a/cli/command/formatter/checkpoint_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/api/types" - "github.com/stretchr/testify/assert" -) - -func TestCheckpointContextFormatWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - { - Context{Format: NewCheckpointFormat(defaultCheckpointFormat)}, - `CHECKPOINT NAME -checkpoint-1 -checkpoint-2 -checkpoint-3 -`, - }, - { - Context{Format: NewCheckpointFormat("{{.Name}}")}, - `checkpoint-1 -checkpoint-2 -checkpoint-3 -`, - }, - { - Context{Format: NewCheckpointFormat("{{.Name}}:")}, - `checkpoint-1: -checkpoint-2: -checkpoint-3: -`, - }, - } - - checkpoints := []types.Checkpoint{ - {"checkpoint-1"}, - {"checkpoint-2"}, - {"checkpoint-3"}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := CheckpointWrite(testcase.context, checkpoints) - if err != nil { - assert.Error(t, err, testcase.expected) - } else { - assert.Equal(t, out.String(), testcase.expected) - } - } -} diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go deleted file mode 100644 index 9b5c24636c..0000000000 --- a/cli/command/formatter/container.go +++ /dev/null @@ -1,259 +0,0 @@ -package formatter - -import ( - "fmt" - "strconv" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - units "github.com/docker/go-units" -) - -const ( - defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" - - containerIDHeader = "CONTAINER ID" - namesHeader = "NAMES" - commandHeader = "COMMAND" - runningForHeader = "CREATED" - statusHeader = "STATUS" - portsHeader = "PORTS" - mountsHeader = "MOUNTS" - localVolumes = "LOCAL VOLUMES" - networksHeader = "NETWORKS" -) - -// NewContainerFormat returns a Format for rendering using a Context -func NewContainerFormat(source string, quiet bool, size bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - format := defaultContainerTableFormat - if size { - format += `\t{{.Size}}` - } - return Format(format) - case RawFormatKey: - if quiet { - return `container_id: {{.ID}}` - } - format := `container_id: {{.ID}} -image: {{.Image}} -command: {{.Command}} -created_at: {{.CreatedAt}} -status: {{- pad .Status 1 0}} -names: {{.Names}} -labels: {{- pad .Labels 1 0}} -ports: {{- pad .Ports 1 0}} -` - if size { - format += `size: {{.Size}}\n` - } - return Format(format) - } - return Format(source) -} - -// ContainerWrite renders the context for a list of containers -func ContainerWrite(ctx Context, containers []types.Container) error { - render := func(format func(subContext subContext) error) error { - for _, container := range containers { - err := format(&containerContext{trunc: ctx.Trunc, c: container}) - if err != nil { - return err - } - } - return nil - } - return ctx.Write(newContainerContext(), render) -} - -type containerHeaderContext map[string]string - -func (c containerHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type containerContext struct { - HeaderContext - trunc bool - c types.Container -} - -func newContainerContext() *containerContext { - containerCtx := containerContext{} - containerCtx.header = containerHeaderContext{ - "ID": containerIDHeader, - "Names": namesHeader, - "Image": imageHeader, - "Command": commandHeader, - "CreatedAt": createdAtHeader, - "RunningFor": runningForHeader, - "Ports": portsHeader, - "Status": statusHeader, - "Size": sizeHeader, - "Labels": labelsHeader, - "Mounts": mountsHeader, - "LocalVolumes": localVolumes, - "Networks": networksHeader, - } - return &containerCtx -} - -func (c *containerContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *containerContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.c.ID) - } - return c.c.ID -} - -func (c *containerContext) Names() string { - names := stripNamePrefix(c.c.Names) - if c.trunc { - for _, name := range names { - if len(strings.Split(name, "/")) == 1 { - names = []string{name} - break - } - } - } - return strings.Join(names, ",") -} - -func (c *containerContext) Image() string { - if c.c.Image == "" { - return "" - } - if c.trunc { - if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { - return trunc - } - // truncate digest if no-trunc option was not selected - ref, err := reference.ParseNormalizedNamed(c.c.Image) - if err == nil { - if nt, ok := ref.(reference.NamedTagged); ok { - // case for when a tag is provided - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - return reference.FamiliarString(namedTagged) - } - } else { - // case for when a tag is not provided - named := reference.TrimNamed(ref) - return reference.FamiliarString(named) - } - } - } - - return c.c.Image -} - -func (c *containerContext) Command() string { - command := c.c.Command - if c.trunc { - command = stringutils.Ellipsis(command, 20) - } - return strconv.Quote(command) -} - -func (c *containerContext) CreatedAt() string { - return time.Unix(int64(c.c.Created), 0).String() -} - -func (c *containerContext) RunningFor() string { - createdAt := time.Unix(int64(c.c.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" -} - -func (c *containerContext) Ports() string { - return api.DisplayablePorts(c.c.Ports) -} - -func (c *containerContext) Status() string { - return c.c.Status -} - -func (c *containerContext) Size() string { - srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) - sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) - - sf := srw - if c.c.SizeRootFs > 0 { - sf = fmt.Sprintf("%s (virtual %s)", srw, sv) - } - return sf -} - -func (c *containerContext) Labels() string { - if c.c.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.c.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *containerContext) Label(name string) string { - if c.c.Labels == nil { - return "" - } - return c.c.Labels[name] -} - -func (c *containerContext) Mounts() string { - var name string - var mounts []string - for _, m := range c.c.Mounts { - if m.Name == "" { - name = m.Source - } else { - name = m.Name - } - if c.trunc { - name = stringutils.Ellipsis(name, 15) - } - mounts = append(mounts, name) - } - return strings.Join(mounts, ",") -} - -func (c *containerContext) LocalVolumes() string { - count := 0 - for _, m := range c.c.Mounts { - if m.Driver == "local" { - count++ - } - } - - return fmt.Sprintf("%d", count) -} - -func (c *containerContext) Networks() string { - if c.c.NetworkSettings == nil { - return "" - } - - networks := []string{} - for k := range c.c.NetworkSettings.Networks { - networks = append(networks, k) - } - - return strings.Join(networks, ",") -} diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go deleted file mode 100644 index 8d23cc781c..0000000000 --- a/cli/command/formatter/container_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestContainerPsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() - unix := time.Now().Add(-65 * time.Second).Unix() - - var ctx containerContext - cases := []struct { - container types.Container - trunc bool - expValue string - call func() string - }{ - {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID}, - {types.Container{ID: containerID}, false, containerID, ctx.ID}, - {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names}, - {types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image}, - {types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image}, - {types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image}, - {types.Container{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - true, - "a5a665ff33ec", - ctx.Image, - }, - {types.Container{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - false, - "a5a665ff33eced1e0803148700880edab4", - ctx.Image, - }, - {types.Container{Image: ""}, true, "", ctx.Image}, - {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, - {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, - {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, - {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, - {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, - {types.Container{}, true, "", ctx.Labels}, - {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, - {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set", - Driver: "local", - Source: "/a/path", - }, - }, - }, true, "this-is-a-lo...", ctx.Mounts}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Driver: "local", - Source: "/a/path", - }, - }, - }, false, "/a/path", ctx.Mounts}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", - Driver: "local", - Source: "/a/path", - }, - }, - }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, - } - - for _, c := range cases { - ctx = containerContext{c: c.container, trunc: c.trunc} - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } - - c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} - ctx = containerContext{c: c1, trunc: true} - - sid := ctx.Label("com.docker.swarm.swarm-id") - node := ctx.Label("com.docker.swarm.node_name") - if sid != "33" { - t.Fatalf("Expected 33, was %s\n", sid) - } - - if node != "ubuntu" { - t.Fatalf("Expected ubuntu, was %s\n", node) - } - - c2 := types.Container{} - ctx = containerContext{c: c2, trunc: true} - - label := ctx.Label("anything.really") - if label != "" { - t.Fatalf("Expected an empty string, was %s", label) - } -} - -func TestContainerContextWrite(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -1).Unix() - expectedTime := time.Unix(unixTime, 0).String() - - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - Context{Format: NewContainerFormat("table", false, true)}, - `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE -containerID1 ubuntu "" 24 hours ago foobar_baz 0B -containerID2 ubuntu "" 24 hours ago foobar_bar 0B -`, - }, - { - Context{Format: NewContainerFormat("table", false, false)}, - `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -containerID1 ubuntu "" 24 hours ago foobar_baz -containerID2 ubuntu "" 24 hours ago foobar_bar -`, - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", false, false)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", false, true)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", true, false)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table", true, false)}, - "containerID1\ncontainerID2\n", - }, - // Raw Format - { - Context{Format: NewContainerFormat("raw", false, false)}, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: - -`, expectedTime, expectedTime), - }, - { - Context{Format: NewContainerFormat("raw", false, true)}, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: -size: 0B - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: -size: 0B - -`, expectedTime, expectedTime), - }, - { - Context{Format: NewContainerFormat("raw", true, false)}, - "container_id: containerID1\ncontainer_id: containerID2\n", - }, - // Custom Format - { - Context{Format: "{{.Image}}"}, - "ubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("{{.Image}}", false, true)}, - "ubuntu\nubuntu\n", - }, - // Special headers for customerized table format - { - Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)}, - `CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS -conta "ubuntu" 24 hours ago//.FOOBAR_BAZ -conta "ubuntu" 24 hours ago//.FOOBAR_BAR -`, - }, - } - - for _, testcase := range cases { - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ContainerWrite(testcase.context, containers) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestContainerContextWriteWithNoContainers(t *testing.T) { - out := bytes.NewBufferString("") - containers := []types.Container{} - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Image}}", - Output: out, - }, - "", - }, - { - Context{ - Format: "table {{.Image}}", - Output: out, - }, - "IMAGE\n", - }, - { - Context{ - Format: NewContainerFormat("{{.Image}}", false, true), - Output: out, - }, - "", - }, - { - Context{ - Format: NewContainerFormat("table {{.Image}}", false, true), - Output: out, - }, - "IMAGE\n", - }, - { - Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, - "IMAGE SIZE\n", - }, - { - Context{ - Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true), - Output: out, - }, - "IMAGE SIZE\n", - }, - } - - for _, context := range contexts { - ContainerWrite(context.context, containers) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} - -func TestContainerContextWriteJSON(t *testing.T) { - unix := time.Now().Add(-65 * time.Second).Unix() - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix}, - } - expectedCreated := time.Unix(unix, 0).String() - expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, - } - out := bytes.NewBufferString("") - err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestContainerContextWriteJSONField(t *testing.T) { - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"}, - } - out := bytes.NewBufferString("") - err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, containers[i].ID, s) - } -} - -func TestContainerBackCompat(t *testing.T) { - containers := []types.Container{{ID: "brewhaha"}} - cases := []string{ - "ID", - "Names", - "Image", - "Command", - "CreatedAt", - "RunningFor", - "Ports", - "Status", - "Size", - "Labels", - "Mounts", - } - buf := bytes.NewBuffer(nil) - for _, c := range cases { - ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} - if err := ContainerWrite(ctx, containers); err != nil { - t.Logf("could not render template for field '%s': %v", c, err) - t.Fail() - } - buf.Reset() - } -} diff --git a/cli/command/formatter/custom.go b/cli/command/formatter/custom.go deleted file mode 100644 index 73487f63ef..0000000000 --- a/cli/command/formatter/custom.go +++ /dev/null @@ -1,35 +0,0 @@ -package formatter - -const ( - imageHeader = "IMAGE" - createdSinceHeader = "CREATED" - createdAtHeader = "CREATED AT" - sizeHeader = "SIZE" - labelsHeader = "LABELS" - nameHeader = "NAME" - driverHeader = "DRIVER" - scopeHeader = "SCOPE" -) - -type subContext interface { - FullHeader() interface{} -} - -// HeaderContext provides the subContext interface for managing headers -type HeaderContext struct { - header interface{} -} - -// FullHeader returns the header as an interface -func (c *HeaderContext) FullHeader() interface{} { - return c.header -} - -func stripNamePrefix(ss []string) []string { - sss := make([]string, len(ss)) - for i, s := range ss { - sss[i] = s[1:] - } - - return sss -} diff --git a/cli/command/formatter/custom_test.go b/cli/command/formatter/custom_test.go deleted file mode 100644 index da42039dca..0000000000 --- a/cli/command/formatter/custom_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package formatter - -import ( - "reflect" - "strings" - "testing" -) - -func compareMultipleValues(t *testing.T, value, expected string) { - // comma-separated values means probably a map input, which won't - // be guaranteed to have the same order as our expected value - // We'll create maps and use reflect.DeepEquals to check instead: - entriesMap := make(map[string]string) - expMap := make(map[string]string) - entries := strings.Split(value, ",") - expectedEntries := strings.Split(expected, ",") - for _, entry := range entries { - keyval := strings.Split(entry, "=") - entriesMap[keyval[0]] = keyval[1] - } - for _, expected := range expectedEntries { - keyval := strings.Split(expected, "=") - expMap[keyval[0]] = keyval[1] - } - if !reflect.DeepEqual(expMap, entriesMap) { - t.Fatalf("Expected entries: %v, got: %v", expected, value) - } -} diff --git a/cli/command/formatter/diff.go b/cli/command/formatter/diff.go deleted file mode 100644 index 9b4681934c..0000000000 --- a/cli/command/formatter/diff.go +++ /dev/null @@ -1,72 +0,0 @@ -package formatter - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/archive" -) - -const ( - defaultDiffTableFormat = "table {{.Type}}\t{{.Path}}" - - changeTypeHeader = "CHANGE TYPE" - pathHeader = "PATH" -) - -// NewDiffFormat returns a format for use with a diff Context -func NewDiffFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultDiffTableFormat - } - return Format(source) -} - -// DiffWrite writes formatted diff using the Context -func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) error { - - render := func(format func(subContext subContext) error) error { - for _, change := range changes { - if err := format(&diffContext{c: change}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newDiffContext(), render) -} - -type diffContext struct { - HeaderContext - c container.ContainerChangeResponseItem -} - -func newDiffContext() *diffContext { - diffCtx := diffContext{} - diffCtx.header = map[string]string{ - "Type": changeTypeHeader, - "Path": pathHeader, - } - return &diffCtx -} - -func (d *diffContext) MarshalJSON() ([]byte, error) { - return marshalJSON(d) -} - -func (d *diffContext) Type() string { - var kind string - switch d.c.Kind { - case archive.ChangeModify: - kind = "C" - case archive.ChangeAdd: - kind = "A" - case archive.ChangeDelete: - kind = "D" - } - return kind - -} - -func (d *diffContext) Path() string { - return d.c.Path -} diff --git a/cli/command/formatter/diff_test.go b/cli/command/formatter/diff_test.go deleted file mode 100644 index 1aa7b53056..0000000000 --- a/cli/command/formatter/diff_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/archive" - "github.com/stretchr/testify/assert" -) - -func TestDiffContextFormatWrite(t *testing.T) { - // Check default output format (verbose and non-verbose mode) for table headers - cases := []struct { - context Context - expected string - }{ - { - Context{Format: NewDiffFormat("table")}, - `CHANGE TYPE PATH -C /var/log/app.log -A /usr/app/app.js -D /usr/app/old_app.js -`, - }, - { - Context{Format: NewDiffFormat("table {{.Path}}")}, - `PATH -/var/log/app.log -/usr/app/app.js -/usr/app/old_app.js -`, - }, - { - Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, - `C: /var/log/app.log -A: /usr/app/app.js -D: /usr/app/old_app.js -`, - }, - } - - diffs := []container.ContainerChangeResponseItem{ - {archive.ChangeModify, "/var/log/app.log"}, - {archive.ChangeAdd, "/usr/app/app.js"}, - {archive.ChangeDelete, "/usr/app/old_app.js"}, - } - - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := DiffWrite(testcase.context, diffs) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go deleted file mode 100644 index 07e39826ed..0000000000 --- a/cli/command/formatter/disk_usage.go +++ /dev/null @@ -1,358 +0,0 @@ -package formatter - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - units "github.com/docker/go-units" -) - -const ( - defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\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}}" - defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}" - defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" - - typeHeader = "TYPE" - totalHeader = "TOTAL" - activeHeader = "ACTIVE" - reclaimableHeader = "RECLAIMABLE" - containersHeader = "CONTAINERS" - sharedSizeHeader = "SHARED SIZE" - uniqueSizeHeader = "UNIQUE SiZE" -) - -// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct. -type DiskUsageContext struct { - Context - Verbose bool - LayersSize int64 - Images []*types.ImageSummary - Containers []*types.Container - Volumes []*types.Volume -} - -func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) { - ctx.buffer = bytes.NewBufferString("") - ctx.header = "" - ctx.Format = Format(format) - ctx.preFormat() - - return ctx.parseFormat() -} - -// -// NewDiskUsageFormat returns a format for rendering an DiskUsageContext -func NewDiskUsageFormat(source string) Format { - switch source { - case TableFormatKey: - format := defaultDiskUsageTableFormat - return Format(format) - case RawFormatKey: - format := `type: {{.Type}} -total: {{.TotalCount}} -active: {{.Active}} -size: {{.Size}} -reclaimable: {{.Reclaimable}} -` - return Format(format) - } - return Format(source) -} - -func (ctx *DiskUsageContext) Write() (err error) { - if ctx.Verbose == false { - ctx.buffer = bytes.NewBufferString("") - ctx.preFormat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return err - } - - err = ctx.contextFormat(tmpl, &diskUsageImagesContext{ - totalSize: ctx.LayersSize, - images: ctx.Images, - }) - if err != nil { - return err - } - err = ctx.contextFormat(tmpl, &diskUsageContainersContext{ - containers: ctx.Containers, - }) - if err != nil { - return err - } - - err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{ - volumes: ctx.Volumes, - }) - if err != nil { - return err - } - - diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}} - diskUsageContainersCtx.header = map[string]string{ - "Type": typeHeader, - "TotalCount": totalHeader, - "Active": activeHeader, - "Size": sizeHeader, - "Reclaimable": reclaimableHeader, - } - ctx.postFormat(tmpl, &diskUsageContainersCtx) - - return err - } - - // First images - tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat) - if err != nil { - return - } - - ctx.Output.Write([]byte("Images space usage:\n\n")) - for _, i := range ctx.Images { - repo := "" - tag := "" - if len(i.RepoTags) > 0 && !isDangling(*i) { - // Only show the first tag - ref, err := reference.ParseNormalizedNamed(i.RepoTags[0]) - if err != nil { - continue - } - if nt, ok := ref.(reference.NamedTagged); ok { - repo = reference.FamiliarName(ref) - tag = nt.Tag() - } - } - - err = ctx.contextFormat(tmpl, &imageContext{ - repo: repo, - tag: tag, - trunc: true, - i: *i, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newImageContext()) - - // Now containers - ctx.Output.Write([]byte("\nContainers space usage:\n\n")) - tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat) - if err != nil { - return - } - for _, c := range ctx.Containers { - // Don't display the virtual size - c.SizeRootFs = 0 - err = ctx.contextFormat(tmpl, &containerContext{ - trunc: true, - c: *c, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newContainerContext()) - - // And volumes - ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n")) - tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat) - if err != nil { - return - } - for _, v := range ctx.Volumes { - err = ctx.contextFormat(tmpl, &volumeContext{ - v: *v, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newVolumeContext()) - return -} - -type diskUsageImagesContext struct { - HeaderContext - totalSize int64 - images []*types.ImageSummary -} - -func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageImagesContext) Type() string { - return "Images" -} - -func (c *diskUsageImagesContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.images)) -} - -func (c *diskUsageImagesContext) Active() string { - used := 0 - for _, i := range c.images { - if i.Containers > 0 { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageImagesContext) Size() string { - return units.HumanSize(float64(c.totalSize)) - -} - -func (c *diskUsageImagesContext) Reclaimable() string { - var used int64 - - for _, i := range c.images { - if i.Containers != 0 { - if i.VirtualSize == -1 || i.SharedSize == -1 { - continue - } - used += i.VirtualSize - i.SharedSize - } - } - - reclaimable := c.totalSize - used - if c.totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize) - } - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} - -type diskUsageContainersContext struct { - HeaderContext - verbose bool - containers []*types.Container -} - -func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageContainersContext) Type() string { - return "Containers" -} - -func (c *diskUsageContainersContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.containers)) -} - -func (c *diskUsageContainersContext) isActive(container types.Container) bool { - return strings.Contains(container.State, "running") || - strings.Contains(container.State, "paused") || - strings.Contains(container.State, "restarting") -} - -func (c *diskUsageContainersContext) Active() string { - used := 0 - for _, container := range c.containers { - if c.isActive(*container) { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageContainersContext) Size() string { - var size int64 - - for _, container := range c.containers { - size += container.SizeRw - } - - return units.HumanSize(float64(size)) -} - -func (c *diskUsageContainersContext) Reclaimable() string { - var reclaimable int64 - var totalSize int64 - - for _, container := range c.containers { - if !c.isActive(*container) { - reclaimable += container.SizeRw - } - totalSize += container.SizeRw - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) - } - - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} - -type diskUsageVolumesContext struct { - HeaderContext - verbose bool - volumes []*types.Volume -} - -func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageVolumesContext) Type() string { - return "Local Volumes" -} - -func (c *diskUsageVolumesContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.volumes)) -} - -func (c *diskUsageVolumesContext) Active() string { - - used := 0 - for _, v := range c.volumes { - if v.UsageData.RefCount > 0 { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageVolumesContext) Size() string { - var size int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - size += v.UsageData.Size - } - } - - return units.HumanSize(float64(size)) -} - -func (c *diskUsageVolumesContext) Reclaimable() string { - var reclaimable int64 - var totalSize int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - if v.UsageData.RefCount == 0 { - reclaimable += v.UsageData.Size - } - totalSize += v.UsageData.Size - } - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) - } - - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} diff --git a/cli/command/formatter/disk_usage_test.go b/cli/command/formatter/disk_usage_test.go deleted file mode 100644 index 302eb2c8f7..0000000000 --- a/cli/command/formatter/disk_usage_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDiskUsageContextFormatWrite(t *testing.T) { - cases := []struct { - context DiskUsageContext - expected string - }{ - // Check default output format (verbose and non-verbose mode) for table headers - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table"), - }, - Verbose: false}, - `TYPE TOTAL ACTIVE SIZE RECLAIMABLE -Images 0 0 0B 0B -Containers 0 0 0B 0B -Local Volumes 0 0 0B 0B -`, - }, - { - DiskUsageContext{Verbose: true}, - `Images space usage: - -REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS - -Containers space usage: - -CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES - -Local Volumes space usage: - -VOLUME NAME LINKS SIZE -`, - }, - // Errors - { - DiskUsageContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - DiskUsageContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table"), - }, - }, - `TYPE TOTAL ACTIVE SIZE RECLAIMABLE -Images 0 0 0B 0B -Containers 0 0 0B 0B -Local Volumes 0 0 0B 0B -`, - }, - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"), - }, - }, - `TYPE ACTIVE -Images 0 -Containers 0 -Local Volumes 0 -`, - }, - // Raw Format - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("raw"), - }, - }, - `type: Images -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -type: Containers -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -type: Local Volumes -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -`, - }, - } - - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - if err := testcase.context.Write(); err != nil { - assert.Equal(t, testcase.expected, err.Error()) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go deleted file mode 100644 index 3f07aee963..0000000000 --- a/cli/command/formatter/formatter.go +++ /dev/null @@ -1,119 +0,0 @@ -package formatter - -import ( - "bytes" - "io" - "strings" - "text/tabwriter" - "text/template" - - "github.com/docker/docker/pkg/templates" - "github.com/pkg/errors" -) - -// Format keys used to specify certain kinds of output formats -const ( - TableFormatKey = "table" - RawFormatKey = "raw" - PrettyFormatKey = "pretty" - - defaultQuietFormat = "{{.ID}}" -) - -// Format is the format string rendered using the Context -type Format string - -// IsTable returns true if the format is a table-type format -func (f Format) IsTable() bool { - return strings.HasPrefix(string(f), TableFormatKey) -} - -// Contains returns true if the format contains the substring -func (f Format) Contains(sub string) bool { - return strings.Contains(string(f), sub) -} - -// Context contains information required by the formatter to print the output as desired. -type Context struct { - // Output is the output stream to which the formatted string is written. - Output io.Writer - // Format is used to choose raw, table or custom format for the output. - Format Format - // Trunc when set to true will truncate the output of certain fields such as Container ID. - Trunc bool - - // internal element - finalFormat string - header interface{} - buffer *bytes.Buffer -} - -func (c *Context) preFormat() { - c.finalFormat = string(c.Format) - - // TODO: handle this in the Format type - if c.Format.IsTable() { - c.finalFormat = c.finalFormat[len(TableFormatKey):] - } - - c.finalFormat = strings.Trim(c.finalFormat, " ") - r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") - c.finalFormat = r.Replace(c.finalFormat) -} - -func (c *Context) parseFormat() (*template.Template, error) { - tmpl, err := templates.Parse(c.finalFormat) - if err != nil { - return tmpl, errors.Errorf("Template parsing error: %v\n", err) - } - return tmpl, err -} - -func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { - if c.Format.IsTable() { - t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) - buffer := bytes.NewBufferString("") - tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader()) - buffer.WriteTo(t) - t.Write([]byte("\n")) - c.buffer.WriteTo(t) - t.Flush() - } else { - c.buffer.WriteTo(c.Output) - } -} - -func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { - if err := tmpl.Execute(c.buffer, subContext); err != nil { - return errors.Errorf("Template parsing error: %v\n", err) - } - if c.Format.IsTable() && c.header != nil { - c.header = subContext.FullHeader() - } - c.buffer.WriteString("\n") - return nil -} - -// SubFormat is a function type accepted by Write() -type SubFormat func(func(subContext) error) error - -// Write the template to the buffer using this Context -func (c *Context) Write(sub subContext, f SubFormat) error { - c.buffer = bytes.NewBufferString("") - c.preFormat() - - tmpl, err := c.parseFormat() - if err != nil { - return err - } - - subFormat := func(subContext subContext) error { - return c.contextFormat(tmpl, subContext) - } - if err := f(subFormat); err != nil { - return err - } - - c.postFormat(tmpl, sub) - return nil -} diff --git a/cli/command/formatter/history.go b/cli/command/formatter/history.go deleted file mode 100644 index 2b7de399a0..0000000000 --- a/cli/command/formatter/history.go +++ /dev/null @@ -1,113 +0,0 @@ -package formatter - -import ( - "strconv" - "strings" - "time" - - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - units "github.com/docker/go-units" -) - -const ( - defaultHistoryTableFormat = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" - nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" - - historyIDHeader = "IMAGE" - createdByHeader = "CREATED BY" - commentHeader = "COMMENT" -) - -// NewHistoryFormat returns a format for rendering an HistoryContext -func NewHistoryFormat(source string, quiet bool, human bool) Format { - switch source { - case TableFormatKey: - switch { - case quiet: - return defaultQuietFormat - case !human: - return nonHumanHistoryTableFormat - default: - return defaultHistoryTableFormat - } - } - - return Format(source) -} - -// HistoryWrite writes the context -func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error { - render := func(format func(subContext subContext) error) error { - for _, history := range histories { - historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} - if err := format(historyCtx); err != nil { - return err - } - } - return nil - } - historyCtx := &historyContext{} - historyCtx.header = map[string]string{ - "ID": historyIDHeader, - "CreatedSince": createdSinceHeader, - "CreatedAt": createdAtHeader, - "CreatedBy": createdByHeader, - "Size": sizeHeader, - "Comment": commentHeader, - } - return ctx.Write(historyCtx, render) -} - -type historyContext struct { - HeaderContext - trunc bool - human bool - h image.HistoryResponseItem -} - -func (c *historyContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *historyContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.h.ID) - } - return c.h.ID -} - -func (c *historyContext) CreatedAt() string { - var created string - created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) - return created -} - -func (c *historyContext) CreatedSince() string { - var created string - created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) - return created + " ago" -} - -func (c *historyContext) CreatedBy() string { - createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1) - if c.trunc { - createdBy = stringutils.Ellipsis(createdBy, 45) - } - return createdBy -} - -func (c *historyContext) Size() string { - size := "" - if c.human { - size = units.HumanSizeWithPrecision(float64(c.h.Size), 3) - } else { - size = strconv.FormatInt(c.h.Size, 10) - } - return size -} - -func (c *historyContext) Comment() string { - return c.h.Comment -} diff --git a/cli/command/formatter/history_test.go b/cli/command/formatter/history_test.go deleted file mode 100644 index ce80dc9b8b..0000000000 --- a/cli/command/formatter/history_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package formatter - -import ( - "strconv" - "strings" - "testing" - "time" - - "bytes" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - "github.com/stretchr/testify/assert" -) - -type historyCase struct { - historyCtx historyContext - expValue string - call func() string -} - -func TestHistoryContext_ID(t *testing.T) { - id := stringid.GenerateRandomID() - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{ID: id}, - trunc: false, - }, id, ctx.ID, - }, - { - historyContext{ - h: image.HistoryResponseItem{ID: id}, - trunc: true, - }, stringid.TruncateID(id), ctx.ID, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_CreatedSince(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -7).Unix() - expected := "7 days ago" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Created: unixTime}, - trunc: false, - human: true, - }, expected, ctx.CreatedSince, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_CreatedBy(t *testing.T) { - withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` - expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{CreatedBy: withTabs}, - trunc: false, - }, expected, ctx.CreatedBy, - }, - { - historyContext{ - h: image.HistoryResponseItem{CreatedBy: withTabs}, - trunc: true, - }, stringutils.Ellipsis(expected, 45), ctx.CreatedBy, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Size(t *testing.T) { - size := int64(182964289) - expected := "183MB" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Size: size}, - trunc: false, - human: true, - }, expected, ctx.Size, - }, { - historyContext{ - h: image.HistoryResponseItem{Size: size}, - trunc: false, - human: false, - }, strconv.Itoa(182964289), ctx.Size, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Comment(t *testing.T) { - comment := "Some comment" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Comment: comment}, - trunc: false, - }, comment, ctx.Comment, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Table(t *testing.T) { - out := bytes.NewBufferString("") - unixTime := time.Now().AddDate(0, 0, -1).Unix() - histories := []image.HistoryResponseItem{ - {ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - } - expectedNoTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT -imageID1 24 hours ago /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on 183MB Hi -imageID2 24 hours ago /bin/bash echo 183MB Hi -imageID3 24 hours ago /bin/bash ls 183MB Hi -imageID4 24 hours ago /bin/bash grep 183MB Hi -` - expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT -imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi -imageID2 24 hours ago /bin/bash echo 183MB Hi -imageID3 24 hours ago /bin/bash ls 183MB Hi -imageID4 24 hours ago /bin/bash grep 183MB Hi -` - - contexts := []struct { - context Context - expected string - }{ - {Context{ - Format: NewHistoryFormat("table", false, true), - Trunc: true, - Output: out, - }, - expectedTrunc, - }, - {Context{ - Format: NewHistoryFormat("table", false, true), - Trunc: false, - Output: out, - }, - expectedNoTrunc, - }, - } - - for _, context := range contexts { - HistoryWrite(context.context, true, histories) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go deleted file mode 100644 index 3aae34ea11..0000000000 --- a/cli/command/formatter/image.go +++ /dev/null @@ -1,272 +0,0 @@ -package formatter - -import ( - "fmt" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" -) - -const ( - defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" - defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" - - imageIDHeader = "IMAGE ID" - repositoryHeader = "REPOSITORY" - tagHeader = "TAG" - digestHeader = "DIGEST" -) - -// ImageContext contains image specific information required by the formatter, encapsulate a Context struct. -type ImageContext struct { - Context - Digest bool -} - -func isDangling(image types.ImageSummary) bool { - return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" -} - -// NewImageFormat returns a format for rendering an ImageContext -func NewImageFormat(source string, quiet bool, digest bool) Format { - switch source { - case TableFormatKey: - switch { - case quiet: - return defaultQuietFormat - case digest: - return defaultImageTableFormatWithDigest - default: - return defaultImageTableFormat - } - case RawFormatKey: - switch { - case quiet: - return `image_id: {{.ID}}` - case digest: - return `repository: {{ .Repository }} -tag: {{.Tag}} -digest: {{.Digest}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - default: - return `repository: {{ .Repository }} -tag: {{.Tag}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - } - } - - format := Format(source) - if format.IsTable() && digest && !format.Contains("{{.Digest}}") { - format += "\t{{.Digest}}" - } - return format -} - -// ImageWrite writes the formatter images using the ImageContext -func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { - render := func(format func(subContext subContext) error) error { - return imageFormat(ctx, images, format) - } - return ctx.Write(newImageContext(), render) -} - -func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { - for _, image := range images { - images := []*imageContext{} - if isDangling(image) { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: "", - tag: "", - digest: "", - }) - } else { - repoTags := map[string][]string{} - repoDigests := map[string][]string{} - - for _, refString := range image.RepoTags { - ref, err := reference.ParseNormalizedNamed(refString) - if err != nil { - continue - } - if nt, ok := ref.(reference.NamedTagged); ok { - familiarRef := reference.FamiliarName(ref) - repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag()) - } - } - for _, refString := range image.RepoDigests { - ref, err := reference.ParseNormalizedNamed(refString) - if err != nil { - continue - } - if c, ok := ref.(reference.Canonical); ok { - familiarRef := reference.FamiliarName(ref) - repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String()) - } - } - - for repo, tags := range repoTags { - digests := repoDigests[repo] - - // Do not display digests as their own row - delete(repoDigests, repo) - - if !ctx.Digest { - // Ignore digest references, just show tag once - digests = nil - } - - for _, tag := range tags { - if len(digests) == 0 { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: "", - }) - continue - } - // Display the digests for each tag - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: dgst, - }) - } - - } - } - - // Show rows for remaining digest only references - for repo, digests := range repoDigests { - // If digests are displayed, show row per digest - if ctx.Digest { - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - digest: dgst, - }) - } - } else { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - }) - } - } - } - for _, imageCtx := range images { - if err := format(imageCtx); err != nil { - return err - } - } - } - return nil -} - -type imageContext struct { - HeaderContext - trunc bool - i types.ImageSummary - repo string - tag string - digest string -} - -func newImageContext() *imageContext { - imageCtx := imageContext{} - imageCtx.header = map[string]string{ - "ID": imageIDHeader, - "Repository": repositoryHeader, - "Tag": tagHeader, - "Digest": digestHeader, - "CreatedSince": createdSinceHeader, - "CreatedAt": createdAtHeader, - "Size": sizeHeader, - "Containers": containersHeader, - "VirtualSize": sizeHeader, - "SharedSize": sharedSizeHeader, - "UniqueSize": uniqueSizeHeader, - } - return &imageCtx -} - -func (c *imageContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *imageContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.i.ID) - } - return c.i.ID -} - -func (c *imageContext) Repository() string { - return c.repo -} - -func (c *imageContext) Tag() string { - return c.tag -} - -func (c *imageContext) Digest() string { - return c.digest -} - -func (c *imageContext) CreatedSince() string { - createdAt := time.Unix(int64(c.i.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" -} - -func (c *imageContext) CreatedAt() string { - return time.Unix(int64(c.i.Created), 0).String() -} - -func (c *imageContext) Size() string { - return units.HumanSizeWithPrecision(float64(c.i.Size), 3) -} - -func (c *imageContext) Containers() string { - if c.i.Containers == -1 { - return "N/A" - } - return fmt.Sprintf("%d", c.i.Containers) -} - -func (c *imageContext) VirtualSize() string { - return units.HumanSize(float64(c.i.VirtualSize)) -} - -func (c *imageContext) SharedSize() string { - if c.i.SharedSize == -1 { - return "N/A" - } - return units.HumanSize(float64(c.i.SharedSize)) -} - -func (c *imageContext) UniqueSize() string { - if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { - return "N/A" - } - return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize)) -} diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go deleted file mode 100644 index b3c4cc8094..0000000000 --- a/cli/command/formatter/image_test.go +++ /dev/null @@ -1,327 +0,0 @@ -package formatter - -import ( - "bytes" - "fmt" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestImageContext(t *testing.T) { - imageID := stringid.GenerateRandomID() - unix := time.Now().Unix() - - var ctx imageContext - cases := []struct { - imageCtx imageContext - expValue string - call func() string - }{ - {imageContext{ - i: types.ImageSummary{ID: imageID}, - trunc: true, - }, stringid.TruncateID(imageID), ctx.ID}, - {imageContext{ - i: types.ImageSummary{ID: imageID}, - trunc: false, - }, imageID, ctx.ID}, - {imageContext{ - i: types.ImageSummary{Size: 10, VirtualSize: 10}, - trunc: true, - }, "10B", ctx.Size}, - {imageContext{ - i: types.ImageSummary{Created: unix}, - trunc: true, - }, time.Unix(unix, 0).String(), ctx.CreatedAt}, - // FIXME - // {imageContext{ - // i: types.ImageSummary{Created: unix}, - // trunc: true, - // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, - {imageContext{ - i: types.ImageSummary{}, - repo: "busybox", - }, "busybox", ctx.Repository}, - {imageContext{ - i: types.ImageSummary{}, - tag: "latest", - }, "latest", ctx.Tag}, - {imageContext{ - i: types.ImageSummary{}, - digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", - }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest}, - } - - for _, c := range cases { - ctx = c.imageCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestImageContextWrite(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -1).Unix() - expectedTime := time.Unix(unixTime, 0).String() - - cases := []struct { - context ImageContext - expected string - }{ - // Errors - { - ImageContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - ImageContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", false, false), - }, - }, - `REPOSITORY TAG IMAGE ID CREATED SIZE -image tag1 imageID1 24 hours ago 0B -image tag2 imageID2 24 hours ago 0B - imageID3 24 hours ago 0B -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, false), - }, - }, - "REPOSITORY\nimage\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, true), - }, - Digest: true, - }, - `REPOSITORY DIGEST -image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf -image - -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", true, false), - }, - }, - "REPOSITORY\nimage\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", true, false), - }, - }, - "imageID1\nimageID2\nimageID3\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", false, true), - }, - Digest: true, - }, - `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE -image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0B -image tag2 imageID2 24 hours ago 0B - imageID3 24 hours ago 0B -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", true, true), - }, - Digest: true, - }, - "imageID1\nimageID2\nimageID3\n", - }, - // Raw Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", false, false), - }, - }, - fmt.Sprintf(`repository: image -tag: tag1 -image_id: imageID1 -created_at: %s -virtual_size: 0B - -repository: image -tag: tag2 -image_id: imageID2 -created_at: %s -virtual_size: 0B - -repository: -tag: -image_id: imageID3 -created_at: %s -virtual_size: 0B - -`, expectedTime, expectedTime, expectedTime), - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", false, true), - }, - Digest: true, - }, - fmt.Sprintf(`repository: image -tag: tag1 -digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf -image_id: imageID1 -created_at: %s -virtual_size: 0B - -repository: image -tag: tag2 -digest: -image_id: imageID2 -created_at: %s -virtual_size: 0B - -repository: -tag: -digest: -image_id: imageID3 -created_at: %s -virtual_size: 0B - -`, expectedTime, expectedTime, expectedTime), - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", true, false), - }, - }, - `image_id: imageID1 -image_id: imageID2 -image_id: imageID3 -`, - }, - // Custom Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, false), - }, - }, - "image\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, true), - }, - Digest: true, - }, - "image\nimage\n\n", - }, - } - - for _, testcase := range cases { - images := []types.ImageSummary{ - {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime}, - {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime}, - {ID: "imageID3", RepoTags: []string{":"}, RepoDigests: []string{"@"}, Created: unixTime}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ImageWrite(testcase.context, images) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestImageContextWriteWithNoImage(t *testing.T) { - out := bytes.NewBufferString("") - images := []types.ImageSummary{} - - contexts := []struct { - context ImageContext - expected string - }{ - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, false), - Output: out, - }, - }, - "", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, false), - Output: out, - }, - }, - "REPOSITORY\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, true), - Output: out, - }, - }, - "", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, true), - Output: out, - }, - }, - "REPOSITORY DIGEST\n", - }, - } - - for _, context := range contexts { - ImageWrite(context.context, images) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/network.go b/cli/command/formatter/network.go deleted file mode 100644 index 4aeebd1750..0000000000 --- a/cli/command/formatter/network.go +++ /dev/null @@ -1,129 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" -) - -const ( - defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}" - - networkIDHeader = "NETWORK ID" - ipv6Header = "IPV6" - internalHeader = "INTERNAL" -) - -// NewNetworkFormat returns a Format for rendering using a network Context -func NewNetworkFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultNetworkTableFormat - case RawFormatKey: - if quiet { - return `network_id: {{.ID}}` - } - return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` - } - return Format(source) -} - -// NetworkWrite writes the context -func NetworkWrite(ctx Context, networks []types.NetworkResource) error { - render := func(format func(subContext subContext) error) error { - for _, network := range networks { - networkCtx := &networkContext{trunc: ctx.Trunc, n: network} - if err := format(networkCtx); err != nil { - return err - } - } - return nil - } - networkCtx := networkContext{} - networkCtx.header = networkHeaderContext{ - "ID": networkIDHeader, - "Name": nameHeader, - "Driver": driverHeader, - "Scope": scopeHeader, - "IPv6": ipv6Header, - "Internal": internalHeader, - "Labels": labelsHeader, - "CreatedAt": createdAtHeader, - } - return ctx.Write(&networkCtx, render) -} - -type networkHeaderContext map[string]string - -func (c networkHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type networkContext struct { - HeaderContext - trunc bool - n types.NetworkResource -} - -func (c *networkContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *networkContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.n.ID) - } - return c.n.ID -} - -func (c *networkContext) Name() string { - return c.n.Name -} - -func (c *networkContext) Driver() string { - return c.n.Driver -} - -func (c *networkContext) Scope() string { - return c.n.Scope -} - -func (c *networkContext) IPv6() string { - return fmt.Sprintf("%v", c.n.EnableIPv6) -} - -func (c *networkContext) Internal() string { - return fmt.Sprintf("%v", c.n.Internal) -} - -func (c *networkContext) Labels() string { - if c.n.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.n.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *networkContext) Label(name string) string { - if c.n.Labels == nil { - return "" - } - return c.n.Labels[name] -} - -func (c *networkContext) CreatedAt() string { - return c.n.Created.String() -} diff --git a/cli/command/formatter/network_test.go b/cli/command/formatter/network_test.go deleted file mode 100644 index b8cab078e7..0000000000 --- a/cli/command/formatter/network_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestNetworkContext(t *testing.T) { - networkID := stringid.GenerateRandomID() - - var ctx networkContext - cases := []struct { - networkCtx networkContext - expValue string - call func() string - }{ - {networkContext{ - n: types.NetworkResource{ID: networkID}, - trunc: false, - }, networkID, ctx.ID}, - {networkContext{ - n: types.NetworkResource{ID: networkID}, - trunc: true, - }, stringid.TruncateID(networkID), ctx.ID}, - {networkContext{ - n: types.NetworkResource{Name: "network_name"}, - }, "network_name", ctx.Name}, - {networkContext{ - n: types.NetworkResource{Driver: "driver_name"}, - }, "driver_name", ctx.Driver}, - {networkContext{ - n: types.NetworkResource{EnableIPv6: true}, - }, "true", ctx.IPv6}, - {networkContext{ - n: types.NetworkResource{EnableIPv6: false}, - }, "false", ctx.IPv6}, - {networkContext{ - n: types.NetworkResource{Internal: true}, - }, "true", ctx.Internal}, - {networkContext{ - n: types.NetworkResource{Internal: false}, - }, "false", ctx.Internal}, - {networkContext{ - n: types.NetworkResource{}, - }, "", ctx.Labels}, - {networkContext{ - n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", ctx.Labels}, - } - - for _, c := range cases { - ctx = c.networkCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestNetworkContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewNetworkFormat("table", false)}, - `NETWORK ID NAME DRIVER SCOPE -networkID1 foobar_baz foo local -networkID2 foobar_bar bar local -`, - }, - { - Context{Format: NewNetworkFormat("table", true)}, - `networkID1 -networkID2 -`, - }, - { - Context{Format: NewNetworkFormat("table {{.Name}}", false)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewNetworkFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewNetworkFormat("raw", false)}, - `network_id: networkID1 -name: foobar_baz -driver: foo -scope: local - -network_id: networkID2 -name: foobar_bar -driver: bar -scope: local - -`, - }, - { - Context{Format: NewNetworkFormat("raw", true)}, - `network_id: networkID1 -network_id: networkID2 -`, - }, - // Custom Format - { - Context{Format: NewNetworkFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - // Custom Format with CreatedAt - { - Context{Format: NewNetworkFormat("{{.Name}} {{.CreatedAt}}", false)}, - `foobar_baz 2016-01-01 00:00:00 +0000 UTC -foobar_bar 2017-01-01 00:00:00 +0000 UTC -`, - }, - } - - timestamp1, _ := time.Parse("2006-01-02", "2016-01-01") - timestamp2, _ := time.Parse("2006-01-02", "2017-01-01") - - for _, testcase := range cases { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, - {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := NetworkWrite(testcase.context, networks) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestNetworkContextWriteJSON(t *testing.T) { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, - {"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, - } - - out := bytes.NewBufferString("") - err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestNetworkContextWriteJSONField(t *testing.T) { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, networks[i].ID, s) - } -} diff --git a/cli/command/formatter/node.go b/cli/command/formatter/node.go deleted file mode 100644 index 4d7f293f5c..0000000000 --- a/cli/command/formatter/node.go +++ /dev/null @@ -1,292 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - units "github.com/docker/go-units" -) - -const ( - defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" - nodeInspectPrettyTemplate Format = `ID: {{.ID}} -{{- if .Name }} -Name: {{.Name}} -{{- end }} -{{- if .Labels }} -Labels: -{{- range $k, $v := .Labels }} - - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{ end }} -Hostname: {{.Hostname}} -Joined at: {{.CreatedAt}} -Status: - State: {{.StatusState}} - {{- if .HasStatusMessage}} - Message: {{.StatusMessage}} - {{- end}} - Availability: {{.SpecAvailability}} - {{- if .Status.Addr}} - Address: {{.StatusAddr}} - {{- end}} -{{- if .HasManagerStatus}} -Manager Status: - Address: {{.ManagerStatusAddr}} - Raft Status: {{.ManagerStatusReachability}} - {{- if .IsManagerStatusLeader}} - Leader: Yes - {{- else}} - Leader: No - {{- end}} -{{- end}} -Platform: - Operating System: {{.PlatformOS}} - Architecture: {{.PlatformArchitecture}} -Resources: - CPUs: {{.ResourceNanoCPUs}} - Memory: {{.ResourceMemory}} -{{- if .HasEnginePlugins}} -Plugins: -{{- range $k, $v := .EnginePlugins }} - {{ $k }}:{{if $v }} {{ $v }}{{ end }} -{{- end }} -{{- end }} -Engine Version: {{.EngineVersion}} -{{- if .EngineLabels}} -Engine Labels: -{{- range $k, $v := .EngineLabels }} - - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{- end }} -` - nodeIDHeader = "ID" - selfHeader = "" - hostnameHeader = "HOSTNAME" - availabilityHeader = "AVAILABILITY" - managerStatusHeader = "MANAGER STATUS" -) - -// NewNodeFormat returns a Format for rendering using a node Context -func NewNodeFormat(source string, quiet bool) Format { - switch source { - case PrettyFormatKey: - return nodeInspectPrettyTemplate - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultNodeTableFormat - case RawFormatKey: - if quiet { - return `node_id: {{.ID}}` - } - return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n` - } - return Format(source) -} - -// NodeWrite writes the context -func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { - render := func(format func(subContext subContext) error) error { - for _, node := range nodes { - nodeCtx := &nodeContext{n: node, info: info} - if err := format(nodeCtx); err != nil { - return err - } - } - return nil - } - nodeCtx := nodeContext{} - nodeCtx.header = nodeHeaderContext{ - "ID": nodeIDHeader, - "Self": selfHeader, - "Hostname": hostnameHeader, - "Status": statusHeader, - "Availability": availabilityHeader, - "ManagerStatus": managerStatusHeader, - } - return ctx.Write(&nodeCtx, render) -} - -type nodeHeaderContext map[string]string - -type nodeContext struct { - HeaderContext - n swarm.Node - info types.Info -} - -func (c *nodeContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *nodeContext) ID() string { - return c.n.ID -} - -func (c *nodeContext) Self() bool { - return c.n.ID == c.info.Swarm.NodeID -} - -func (c *nodeContext) Hostname() string { - return c.n.Description.Hostname -} - -func (c *nodeContext) Status() string { - return command.PrettyPrint(string(c.n.Status.State)) -} - -func (c *nodeContext) Availability() string { - return command.PrettyPrint(string(c.n.Spec.Availability)) -} - -func (c *nodeContext) ManagerStatus() string { - reachability := "" - if c.n.ManagerStatus != nil { - if c.n.ManagerStatus.Leader { - reachability = "Leader" - } else { - reachability = string(c.n.ManagerStatus.Reachability) - } - } - return command.PrettyPrint(reachability) -} - -// NodeInspectWrite renders the context for a list of services -func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != nodeInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) - } - render := func(format func(subContext subContext) error) error { - for _, ref := range refs { - nodeI, _, err := getRef(ref) - if err != nil { - return err - } - node, ok := nodeI.(swarm.Node) - if !ok { - return fmt.Errorf("got wrong object to inspect :%v", ok) - } - if err := format(&nodeInspectContext{Node: node}); err != nil { - return err - } - } - return nil - } - return ctx.Write(&nodeInspectContext{}, render) -} - -type nodeInspectContext struct { - swarm.Node - subContext -} - -func (ctx *nodeInspectContext) ID() string { - return ctx.Node.ID -} - -func (ctx *nodeInspectContext) Name() string { - return ctx.Node.Spec.Name -} - -func (ctx *nodeInspectContext) Labels() map[string]string { - return ctx.Node.Spec.Labels -} - -func (ctx *nodeInspectContext) Hostname() string { - return ctx.Node.Description.Hostname -} - -func (ctx *nodeInspectContext) CreatedAt() string { - return command.PrettyPrint(ctx.Node.CreatedAt) -} - -func (ctx *nodeInspectContext) StatusState() string { - return command.PrettyPrint(ctx.Node.Status.State) -} - -func (ctx *nodeInspectContext) HasStatusMessage() bool { - return ctx.Node.Status.Message != "" -} - -func (ctx *nodeInspectContext) StatusMessage() string { - return command.PrettyPrint(ctx.Node.Status.Message) -} - -func (ctx *nodeInspectContext) SpecAvailability() string { - return command.PrettyPrint(ctx.Node.Spec.Availability) -} - -func (ctx *nodeInspectContext) HasStatusAddr() bool { - return ctx.Node.Status.Addr != "" -} - -func (ctx *nodeInspectContext) StatusAddr() string { - return ctx.Node.Status.Addr -} - -func (ctx *nodeInspectContext) HasManagerStatus() bool { - return ctx.Node.ManagerStatus != nil -} - -func (ctx *nodeInspectContext) ManagerStatusAddr() string { - return ctx.Node.ManagerStatus.Addr -} - -func (ctx *nodeInspectContext) ManagerStatusReachability() string { - return command.PrettyPrint(ctx.Node.ManagerStatus.Reachability) -} - -func (ctx *nodeInspectContext) IsManagerStatusLeader() bool { - return ctx.Node.ManagerStatus.Leader -} - -func (ctx *nodeInspectContext) PlatformOS() string { - return ctx.Node.Description.Platform.OS -} - -func (ctx *nodeInspectContext) PlatformArchitecture() string { - return ctx.Node.Description.Platform.Architecture -} - -func (ctx *nodeInspectContext) ResourceNanoCPUs() int { - if ctx.Node.Description.Resources.NanoCPUs == 0 { - return int(0) - } - return int(ctx.Node.Description.Resources.NanoCPUs) / 1e9 -} - -func (ctx *nodeInspectContext) ResourceMemory() string { - if ctx.Node.Description.Resources.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Node.Description.Resources.MemoryBytes)) -} - -func (ctx *nodeInspectContext) HasEnginePlugins() bool { - return len(ctx.Node.Description.Engine.Plugins) > 0 -} - -func (ctx *nodeInspectContext) EnginePlugins() map[string]string { - pluginMap := map[string][]string{} - for _, p := range ctx.Node.Description.Engine.Plugins { - pluginMap[p.Type] = append(pluginMap[p.Type], p.Name) - } - - pluginNamesByType := map[string]string{} - for k, v := range pluginMap { - pluginNamesByType[k] = strings.Join(v, ", ") - } - return pluginNamesByType -} - -func (ctx *nodeInspectContext) EngineLabels() map[string]string { - return ctx.Node.Description.Engine.Labels -} - -func (ctx *nodeInspectContext) EngineVersion() string { - return ctx.Node.Description.Engine.EngineVersion -} diff --git a/cli/command/formatter/node_test.go b/cli/command/formatter/node_test.go deleted file mode 100644 index ea2f4ce4d5..0000000000 --- a/cli/command/formatter/node_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestNodeContext(t *testing.T) { - nodeID := stringid.GenerateRandomID() - - var ctx nodeContext - cases := []struct { - nodeCtx nodeContext - expValue string - call func() string - }{ - {nodeContext{ - n: swarm.Node{ID: nodeID}, - }, nodeID, ctx.ID}, - {nodeContext{ - n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}}, - }, "node_hostname", ctx.Hostname}, - {nodeContext{ - n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}}, - }, "Foo", ctx.Status}, - {nodeContext{ - n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}}, - }, "Drain", ctx.Availability}, - {nodeContext{ - n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}}, - }, "Leader", ctx.ManagerStatus}, - } - - for _, c := range cases { - ctx = c.nodeCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestNodeContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewNodeFormat("table", false)}, - `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS -nodeID1 foobar_baz Foo Drain Leader -nodeID2 foobar_bar Bar Active Reachable -`, - }, - { - Context{Format: NewNodeFormat("table", true)}, - `nodeID1 -nodeID2 -`, - }, - { - Context{Format: NewNodeFormat("table {{.Hostname}}", false)}, - `HOSTNAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewNodeFormat("table {{.Hostname}}", true)}, - `HOSTNAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewNodeFormat("raw", false)}, - `node_id: nodeID1 -hostname: foobar_baz -status: Foo -availability: Drain -manager_status: Leader - -node_id: nodeID2 -hostname: foobar_bar -status: Bar -availability: Active -manager_status: Reachable - -`, - }, - { - Context{Format: NewNodeFormat("raw", true)}, - `node_id: nodeID1 -node_id: nodeID2 -`, - }, - // Custom Format - { - Context{Format: NewNodeFormat("{{.Hostname}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, ManagerStatus: &swarm.ManagerStatus{Leader: true}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, ManagerStatus: &swarm.ManagerStatus{Leader: false, Reachability: swarm.Reachability("Reachable")}}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := NodeWrite(testcase.context, nodes, types.Info{}) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestNodeContextWriteJSON(t *testing.T) { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, - } - expectedJSONs := []map[string]interface{}{ - {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false}, - {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false}, - } - - out := bytes.NewBufferString("") - err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestNodeContextWriteJSONField(t *testing.T) { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, - } - out := bytes.NewBufferString("") - err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, nodes[i].ID, s) - } -} diff --git a/cli/command/formatter/plugin.go b/cli/command/formatter/plugin.go deleted file mode 100644 index 2b71281a58..0000000000 --- a/cli/command/formatter/plugin.go +++ /dev/null @@ -1,95 +0,0 @@ -package formatter - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" -) - -const ( - defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}" - - pluginIDHeader = "ID" - descriptionHeader = "DESCRIPTION" - enabledHeader = "ENABLED" -) - -// NewPluginFormat returns a Format for rendering using a plugin Context -func NewPluginFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultPluginTableFormat - case RawFormatKey: - if quiet { - return `plugin_id: {{.ID}}` - } - return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` - } - return Format(source) -} - -// PluginWrite writes the context -func PluginWrite(ctx Context, plugins []*types.Plugin) error { - render := func(format func(subContext subContext) error) error { - for _, plugin := range plugins { - pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} - if err := format(pluginCtx); err != nil { - return err - } - } - return nil - } - pluginCtx := pluginContext{} - pluginCtx.header = map[string]string{ - "ID": pluginIDHeader, - "Name": nameHeader, - "Description": descriptionHeader, - "Enabled": enabledHeader, - "PluginReference": imageHeader, - } - return ctx.Write(&pluginCtx, render) -} - -type pluginContext struct { - HeaderContext - trunc bool - p types.Plugin -} - -func (c *pluginContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *pluginContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.p.ID) - } - return c.p.ID -} - -func (c *pluginContext) Name() string { - return c.p.Name -} - -func (c *pluginContext) Description() string { - desc := strings.Replace(c.p.Config.Description, "\n", "", -1) - desc = strings.Replace(desc, "\r", "", -1) - if c.trunc { - desc = stringutils.Ellipsis(desc, 45) - } - - return desc -} - -func (c *pluginContext) Enabled() bool { - return c.p.Enabled -} - -func (c *pluginContext) PluginReference() string { - return c.p.PluginReference -} diff --git a/cli/command/formatter/plugin_test.go b/cli/command/formatter/plugin_test.go deleted file mode 100644 index 607262dcc9..0000000000 --- a/cli/command/formatter/plugin_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestPluginContext(t *testing.T) { - pluginID := stringid.GenerateRandomID() - - var ctx pluginContext - cases := []struct { - pluginCtx pluginContext - expValue string - call func() string - }{ - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: false, - }, pluginID, ctx.ID}, - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: true, - }, stringid.TruncateID(pluginID), ctx.ID}, - {pluginContext{ - p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", ctx.Name}, - {pluginContext{ - p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", ctx.Description}, - } - - for _, c := range cases { - ctx = c.pluginCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestPluginContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewPluginFormat("table", false)}, - `ID NAME DESCRIPTION ENABLED -pluginID1 foobar_baz description 1 true -pluginID2 foobar_bar description 2 false -`, - }, - { - Context{Format: NewPluginFormat("table", true)}, - `pluginID1 -pluginID2 -`, - }, - { - Context{Format: NewPluginFormat("table {{.Name}}", false)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewPluginFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewPluginFormat("raw", false)}, - `plugin_id: pluginID1 -name: foobar_baz -description: description 1 -enabled: true - -plugin_id: pluginID2 -name: foobar_bar -description: description 2 -enabled: false - -`, - }, - { - Context{Format: NewPluginFormat("raw", true)}, - `plugin_id: pluginID1 -plugin_id: pluginID2 -`, - }, - // Custom Format - { - Context{Format: NewPluginFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true}, - {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := PluginWrite(testcase.context, plugins) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestPluginContextWriteJSON(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""}, - {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""}, - } - - out := bytes.NewBufferString("") - err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestPluginContextWriteJSONField(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, plugins[i].ID, s) - } -} diff --git a/cli/command/formatter/reflect.go b/cli/command/formatter/reflect.go deleted file mode 100644 index fd59404d05..0000000000 --- a/cli/command/formatter/reflect.go +++ /dev/null @@ -1,66 +0,0 @@ -package formatter - -import ( - "encoding/json" - "reflect" - "unicode" - - "github.com/pkg/errors" -) - -func marshalJSON(x interface{}) ([]byte, error) { - m, err := marshalMap(x) - if err != nil { - return nil, err - } - return json.Marshal(m) -} - -// marshalMap marshals x to map[string]interface{} -func marshalMap(x interface{}) (map[string]interface{}, error) { - val := reflect.ValueOf(x) - if val.Kind() != reflect.Ptr { - return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind()) - } - if val.IsNil() { - return nil, errors.Errorf("expected a pointer to a struct, got nil pointer") - } - valElem := val.Elem() - if valElem.Kind() != reflect.Struct { - return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) - } - typ := val.Type() - m := make(map[string]interface{}) - for i := 0; i < val.NumMethod(); i++ { - k, v, err := marshalForMethod(typ.Method(i), val.Method(i)) - if err != nil { - return nil, err - } - if k != "" { - m[k] = v - } - } - return m, nil -} - -var unmarshallableNames = map[string]struct{}{"FullHeader": {}} - -// marshalForMethod returns the map key and the map value for marshalling the method. -// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()") -func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) { - if val.Kind() != reflect.Func { - return "", nil, errors.Errorf("expected func, got %v", val.Kind()) - } - name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut() - _, blackListed := unmarshallableNames[name] - // FIXME: In text/template, (numOut == 2) is marshallable, - // if the type of the second param is error. - marshallable := unicode.IsUpper(rune(name[0])) && !blackListed && - numIn == 0 && numOut == 1 - if !marshallable { - return "", nil, nil - } - result := val.Call(make([]reflect.Value, numIn)) - intf := result[0].Interface() - return name, intf, nil -} diff --git a/cli/command/formatter/reflect_test.go b/cli/command/formatter/reflect_test.go deleted file mode 100644 index e547b18411..0000000000 --- a/cli/command/formatter/reflect_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package formatter - -import ( - "reflect" - "testing" -) - -type dummy struct { -} - -func (d *dummy) Func1() string { - return "Func1" -} - -func (d *dummy) func2() string { - return "func2(should not be marshalled)" -} - -func (d *dummy) Func3() (string, int) { - return "Func3(should not be marshalled)", -42 -} - -func (d *dummy) Func4() int { - return 4 -} - -type dummyType string - -func (d *dummy) Func5() dummyType { - return dummyType("Func5") -} - -func (d *dummy) FullHeader() string { - return "FullHeader(should not be marshalled)" -} - -var dummyExpected = map[string]interface{}{ - "Func1": "Func1", - "Func4": 4, - "Func5": dummyType("Func5"), -} - -func TestMarshalMap(t *testing.T) { - d := dummy{} - m, err := marshalMap(&d) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(dummyExpected, m) { - t.Fatalf("expected %+v, got %+v", - dummyExpected, m) - } -} - -func TestMarshalMapBad(t *testing.T) { - if _, err := marshalMap(nil); err == nil { - t.Fatal("expected an error (argument is nil)") - } - if _, err := marshalMap(dummy{}); err == nil { - t.Fatal("expected an error (argument is non-pointer)") - } - x := 42 - if _, err := marshalMap(&x); err == nil { - t.Fatal("expected an error (argument is a pointer to non-struct)") - } -} diff --git a/cli/command/formatter/secret.go b/cli/command/formatter/secret.go deleted file mode 100644 index 7ec6f9a62e..0000000000 --- a/cli/command/formatter/secret.go +++ /dev/null @@ -1,101 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/api/types/swarm" - units "github.com/docker/go-units" -) - -const ( - defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" - secretIDHeader = "ID" - secretNameHeader = "NAME" - secretCreatedHeader = "CREATED" - secretUpdatedHeader = "UPDATED" -) - -// NewSecretFormat returns a Format for rendering using a network Context -func NewSecretFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultSecretTableFormat - } - return Format(source) -} - -// SecretWrite writes the context -func SecretWrite(ctx Context, secrets []swarm.Secret) error { - render := func(format func(subContext subContext) error) error { - for _, secret := range secrets { - secretCtx := &secretContext{s: secret} - if err := format(secretCtx); err != nil { - return err - } - } - return nil - } - return ctx.Write(newSecretContext(), render) -} - -func newSecretContext() *secretContext { - sCtx := &secretContext{} - - sCtx.header = map[string]string{ - "ID": secretIDHeader, - "Name": nameHeader, - "CreatedAt": secretCreatedHeader, - "UpdatedAt": secretUpdatedHeader, - "Labels": labelsHeader, - } - return sCtx -} - -type secretContext struct { - HeaderContext - s swarm.Secret -} - -func (c *secretContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *secretContext) ID() string { - return c.s.ID -} - -func (c *secretContext) Name() string { - return c.s.Spec.Annotations.Name -} - -func (c *secretContext) CreatedAt() string { - return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago" -} - -func (c *secretContext) UpdatedAt() string { - return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago" -} - -func (c *secretContext) Labels() string { - mapLabels := c.s.Spec.Annotations.Labels - if mapLabels == nil { - return "" - } - var joinLabels []string - for k, v := range mapLabels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *secretContext) Label(name string) string { - if c.s.Spec.Annotations.Labels == nil { - return "" - } - return c.s.Spec.Annotations.Labels[name] -} diff --git a/cli/command/formatter/secret_test.go b/cli/command/formatter/secret_test.go deleted file mode 100644 index 98fe61315f..0000000000 --- a/cli/command/formatter/secret_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - "time" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestSecretContextFormatWrite(t *testing.T) { - // Check default output format (verbose and non-verbose mode) for table headers - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - {Context{Format: NewSecretFormat("table", false)}, - `ID NAME CREATED UPDATED -1 passwords Less than a second ago Less than a second ago -2 id_rsa Less than a second ago Less than a second ago -`}, - {Context{Format: NewSecretFormat("table {{.Name}}", true)}, - `NAME -passwords -id_rsa -`}, - {Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)}, - `1-passwords -2-id_rsa -`}, - } - - secrets := []swarm.Secret{ - {ID: "1", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "passwords"}}}, - {ID: "2", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - if err := SecretWrite(testcase.context, secrets); err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/service.go b/cli/command/formatter/service.go deleted file mode 100644 index e32704f337..0000000000 --- a/cli/command/formatter/service.go +++ /dev/null @@ -1,535 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command/inspect" - "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" - "github.com/pkg/errors" -) - -const serviceInspectPrettyTemplate Format = ` -ID: {{.ID}} -Name: {{.Name}} -{{- if .Labels }} -Labels: -{{- range $k, $v := .Labels }} - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{ end }} -Service Mode: -{{- if .IsModeGlobal }} Global -{{- else if .IsModeReplicated }} Replicated -{{- if .ModeReplicatedReplicas }} - Replicas: {{ .ModeReplicatedReplicas }} -{{- end }}{{ end }} -{{- if .HasUpdateStatus }} -UpdateStatus: - State: {{ .UpdateStatusState }} -{{- if .HasUpdateStatusStarted }} - Started: {{ .UpdateStatusStarted }} -{{- end }} -{{- if .UpdateIsCompleted }} - Completed: {{ .UpdateStatusCompleted }} -{{- end }} - Message: {{ .UpdateStatusMessage }} -{{- end }} -Placement: -{{- if .TaskPlacementConstraints }} - Constraints: {{ .TaskPlacementConstraints }} -{{- end }} -{{- if .TaskPlacementPreferences }} - Preferences: {{ .TaskPlacementPreferences }} -{{- end }} -{{- if .HasUpdateConfig }} -UpdateConfig: - Parallelism: {{ .UpdateParallelism }} -{{- if .HasUpdateDelay}} - Delay: {{ .UpdateDelay }} -{{- end }} - On failure: {{ .UpdateOnFailure }} -{{- if .HasUpdateMonitor}} - Monitoring Period: {{ .UpdateMonitor }} -{{- end }} - Max failure ratio: {{ .UpdateMaxFailureRatio }} - Update order: {{ .UpdateOrder }} -{{- end }} -{{- if .HasRollbackConfig }} -RollbackConfig: - Parallelism: {{ .RollbackParallelism }} -{{- if .HasRollbackDelay}} - Delay: {{ .RollbackDelay }} -{{- end }} - On failure: {{ .RollbackOnFailure }} -{{- if .HasRollbackMonitor}} - Monitoring Period: {{ .RollbackMonitor }} -{{- end }} - Max failure ratio: {{ .RollbackMaxFailureRatio }} - Rollback order: {{ .RollbackOrder }} -{{- end }} -ContainerSpec: - Image: {{ .ContainerImage }} -{{- if .ContainerArgs }} - Args: {{ range $arg := .ContainerArgs }}{{ $arg }} {{ end }} -{{- end -}} -{{- if .ContainerEnv }} - Env: {{ range $env := .ContainerEnv }}{{ $env }} {{ end }} -{{- end -}} -{{- if .ContainerWorkDir }} - Dir: {{ .ContainerWorkDir }} -{{- end -}} -{{- if .ContainerUser }} - User: {{ .ContainerUser }} -{{- end }} -{{- if .ContainerMounts }} -Mounts: -{{- end }} -{{- range $mount := .ContainerMounts }} - Target = {{ $mount.Target }} - Source = {{ $mount.Source }} - ReadOnly = {{ $mount.ReadOnly }} - Type = {{ $mount.Type }} -{{- end -}} -{{- if .HasResources }} -Resources: -{{- if .HasResourceReservations }} - Reservations: -{{- if gt .ResourceReservationNanoCPUs 0.0 }} - CPU: {{ .ResourceReservationNanoCPUs }} -{{- end }} -{{- if .ResourceReservationMemory }} - Memory: {{ .ResourceReservationMemory }} -{{- end }}{{ end }} -{{- if .HasResourceLimits }} - Limits: -{{- if gt .ResourceLimitsNanoCPUs 0.0 }} - CPU: {{ .ResourceLimitsNanoCPUs }} -{{- end }} -{{- if .ResourceLimitMemory }} - Memory: {{ .ResourceLimitMemory }} -{{- end }}{{ end }}{{ end }} -{{- if .Networks }} -Networks: -{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} -Endpoint Mode: {{ .EndpointMode }} -{{- if .Ports }} -Ports: -{{- range $port := .Ports }} - PublishedPort = {{ $port.PublishedPort }} - Protocol = {{ $port.Protocol }} - TargetPort = {{ $port.TargetPort }} - PublishMode = {{ $port.PublishMode }} -{{- end }} {{ end -}} -` - -// NewServiceFormat returns a Format for rendering using a Context -func NewServiceFormat(source string) Format { - switch source { - case PrettyFormatKey: - return serviceInspectPrettyTemplate - default: - return Format(strings.TrimPrefix(source, RawFormatKey)) - } -} - -func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[string]string { - networkNames := make(map[string]string) - for _, network := range service.Spec.TaskTemplate.Networks { - if resolved, _, err := getNetwork(network.Target); err == nil { - if resolvedNetwork, ok := resolved.(types.NetworkResource); ok { - networkNames[resolvedNetwork.ID] = resolvedNetwork.Name - } - } - } - return networkNames -} - -// ServiceInspectWrite renders the context for a list of services -func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { - if ctx.Format != serviceInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) - } - render := func(format func(subContext subContext) error) error { - for _, ref := range refs { - serviceI, _, err := getRef(ref) - if err != nil { - return err - } - service, ok := serviceI.(swarm.Service) - if !ok { - return errors.Errorf("got wrong object to inspect") - } - if err := format(&serviceInspectContext{Service: service, networkNames: resolveNetworks(service, getNetwork)}); err != nil { - return err - } - } - return nil - } - return ctx.Write(&serviceInspectContext{}, render) -} - -type serviceInspectContext struct { - swarm.Service - subContext - - // networkNames is a map from network IDs (as found in - // Networks[x].Target) to network names. - networkNames map[string]string -} - -func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) { - return marshalJSON(ctx) -} - -func (ctx *serviceInspectContext) ID() string { - return ctx.Service.ID -} - -func (ctx *serviceInspectContext) Name() string { - return ctx.Service.Spec.Name -} - -func (ctx *serviceInspectContext) Labels() map[string]string { - return ctx.Service.Spec.Labels -} - -func (ctx *serviceInspectContext) IsModeGlobal() bool { - return ctx.Service.Spec.Mode.Global != nil -} - -func (ctx *serviceInspectContext) IsModeReplicated() bool { - return ctx.Service.Spec.Mode.Replicated != nil -} - -func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { - return ctx.Service.Spec.Mode.Replicated.Replicas -} - -func (ctx *serviceInspectContext) HasUpdateStatus() bool { - return ctx.Service.UpdateStatus != nil && ctx.Service.UpdateStatus.State != "" -} - -func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState { - return ctx.Service.UpdateStatus.State -} - -func (ctx *serviceInspectContext) HasUpdateStatusStarted() bool { - return ctx.Service.UpdateStatus.StartedAt != nil -} - -func (ctx *serviceInspectContext) UpdateStatusStarted() string { - return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.StartedAt)) + " ago" -} - -func (ctx *serviceInspectContext) UpdateIsCompleted() bool { - return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted && ctx.Service.UpdateStatus.CompletedAt != nil -} - -func (ctx *serviceInspectContext) UpdateStatusCompleted() string { - return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.CompletedAt)) + " ago" -} - -func (ctx *serviceInspectContext) UpdateStatusMessage() string { - return ctx.Service.UpdateStatus.Message -} - -func (ctx *serviceInspectContext) TaskPlacementConstraints() []string { - if ctx.Service.Spec.TaskTemplate.Placement != nil { - return ctx.Service.Spec.TaskTemplate.Placement.Constraints - } - return nil -} - -func (ctx *serviceInspectContext) TaskPlacementPreferences() []string { - if ctx.Service.Spec.TaskTemplate.Placement == nil { - return nil - } - var strings []string - for _, pref := range ctx.Service.Spec.TaskTemplate.Placement.Preferences { - if pref.Spread != nil { - strings = append(strings, "spread="+pref.Spread.SpreadDescriptor) - } - } - return strings -} - -func (ctx *serviceInspectContext) HasUpdateConfig() bool { - return ctx.Service.Spec.UpdateConfig != nil -} - -func (ctx *serviceInspectContext) UpdateParallelism() uint64 { - return ctx.Service.Spec.UpdateConfig.Parallelism -} - -func (ctx *serviceInspectContext) HasUpdateDelay() bool { - return ctx.Service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) UpdateDelay() time.Duration { - return ctx.Service.Spec.UpdateConfig.Delay -} - -func (ctx *serviceInspectContext) UpdateOnFailure() string { - return ctx.Service.Spec.UpdateConfig.FailureAction -} - -func (ctx *serviceInspectContext) UpdateOrder() string { - return ctx.Service.Spec.UpdateConfig.Order -} - -func (ctx *serviceInspectContext) HasUpdateMonitor() bool { - return ctx.Service.Spec.UpdateConfig.Monitor.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) UpdateMonitor() time.Duration { - return ctx.Service.Spec.UpdateConfig.Monitor -} - -func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 { - return ctx.Service.Spec.UpdateConfig.MaxFailureRatio -} - -func (ctx *serviceInspectContext) HasRollbackConfig() bool { - return ctx.Service.Spec.RollbackConfig != nil -} - -func (ctx *serviceInspectContext) RollbackParallelism() uint64 { - return ctx.Service.Spec.RollbackConfig.Parallelism -} - -func (ctx *serviceInspectContext) HasRollbackDelay() bool { - return ctx.Service.Spec.RollbackConfig.Delay.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) RollbackDelay() time.Duration { - return ctx.Service.Spec.RollbackConfig.Delay -} - -func (ctx *serviceInspectContext) RollbackOnFailure() string { - return ctx.Service.Spec.RollbackConfig.FailureAction -} - -func (ctx *serviceInspectContext) HasRollbackMonitor() bool { - return ctx.Service.Spec.RollbackConfig.Monitor.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) RollbackMonitor() time.Duration { - return ctx.Service.Spec.RollbackConfig.Monitor -} - -func (ctx *serviceInspectContext) RollbackMaxFailureRatio() float32 { - return ctx.Service.Spec.RollbackConfig.MaxFailureRatio -} - -func (ctx *serviceInspectContext) RollbackOrder() string { - return ctx.Service.Spec.RollbackConfig.Order -} - -func (ctx *serviceInspectContext) ContainerImage() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image -} - -func (ctx *serviceInspectContext) ContainerArgs() []string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Args -} - -func (ctx *serviceInspectContext) ContainerEnv() []string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Env -} - -func (ctx *serviceInspectContext) ContainerWorkDir() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Dir -} - -func (ctx *serviceInspectContext) ContainerUser() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.User -} - -func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts -} - -func (ctx *serviceInspectContext) HasResources() bool { - return ctx.Service.Spec.TaskTemplate.Resources != nil -} - -func (ctx *serviceInspectContext) HasResourceReservations() bool { - if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Reservations == nil { - return false - } - return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0 -} - -func (ctx *serviceInspectContext) ResourceReservationNanoCPUs() float64 { - if ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs == 0 { - return float64(0) - } - return float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs) / 1e9 -} - -func (ctx *serviceInspectContext) ResourceReservationMemory() string { - if ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes)) -} - -func (ctx *serviceInspectContext) HasResourceLimits() bool { - if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Limits == nil { - return false - } - return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0 -} - -func (ctx *serviceInspectContext) ResourceLimitsNanoCPUs() float64 { - return float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs) / 1e9 -} - -func (ctx *serviceInspectContext) ResourceLimitMemory() string { - if ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes)) -} - -func (ctx *serviceInspectContext) Networks() []string { - var out []string - for _, n := range ctx.Service.Spec.TaskTemplate.Networks { - if name, ok := ctx.networkNames[n.Target]; ok { - out = append(out, name) - } else { - out = append(out, n.Target) - } - } - return out -} - -func (ctx *serviceInspectContext) EndpointMode() string { - if ctx.Service.Spec.EndpointSpec == nil { - return "" - } - - return string(ctx.Service.Spec.EndpointSpec.Mode) -} - -func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { - return ctx.Service.Endpoint.Ports -} - -const ( - defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}" - - serviceIDHeader = "ID" - modeHeader = "MODE" - replicasHeader = "REPLICAS" -) - -// NewServiceListFormat returns a Format for rendering using a service Context -func NewServiceListFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultServiceTableFormat - case RawFormatKey: - if quiet { - return `id: {{.ID}}` - } - return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n` - } - return Format(source) -} - -// ServiceListInfo stores the information about mode and replicas to be used by template -type ServiceListInfo struct { - Mode string - Replicas string -} - -// ServiceListWrite writes the context -func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error { - render := func(format func(subContext subContext) error) error { - for _, service := range services { - serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas} - if err := format(serviceCtx); err != nil { - return err - } - } - return nil - } - serviceCtx := serviceContext{} - serviceCtx.header = map[string]string{ - "ID": serviceIDHeader, - "Name": nameHeader, - "Mode": modeHeader, - "Replicas": replicasHeader, - "Image": imageHeader, - "Ports": portsHeader, - } - return ctx.Write(&serviceCtx, render) -} - -type serviceContext struct { - HeaderContext - service swarm.Service - mode string - replicas string -} - -func (c *serviceContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *serviceContext) ID() string { - return stringid.TruncateID(c.service.ID) -} - -func (c *serviceContext) Name() string { - return c.service.Spec.Name -} - -func (c *serviceContext) Mode() string { - return c.mode -} - -func (c *serviceContext) Replicas() string { - return c.replicas -} - -func (c *serviceContext) Image() string { - image := c.service.Spec.TaskTemplate.ContainerSpec.Image - if ref, err := reference.ParseNormalizedNamed(image); err == nil { - // update image string for display, (strips any digest) - if nt, ok := ref.(reference.NamedTagged); ok { - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - image = reference.FamiliarString(namedTagged) - } - } - } - - return image -} - -func (c *serviceContext) Ports() string { - if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil { - return "" - } - ports := []string{} - for _, pConfig := range c.service.Spec.EndpointSpec.Ports { - if pConfig.PublishMode == swarm.PortConfigPublishModeIngress { - ports = append(ports, fmt.Sprintf("*:%d->%d/%s", - pConfig.PublishedPort, - pConfig.TargetPort, - pConfig.Protocol, - )) - } - } - return strings.Join(ports, ",") -} diff --git a/cli/command/formatter/service_test.go b/cli/command/formatter/service_test.go deleted file mode 100644 index 629f853930..0000000000 --- a/cli/command/formatter/service_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestServiceContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewServiceListFormat("table", false)}, - `ID NAME MODE REPLICAS IMAGE PORTS -id_baz baz global 2/4 *:80->8080/tcp -id_bar bar replicated 2/4 *:80->8080/tcp -`, - }, - { - Context{Format: NewServiceListFormat("table", true)}, - `id_baz -id_bar -`, - }, - { - Context{Format: NewServiceListFormat("table {{.Name}}", false)}, - `NAME -baz -bar -`, - }, - { - Context{Format: NewServiceListFormat("table {{.Name}}", true)}, - `NAME -baz -bar -`, - }, - // Raw Format - { - Context{Format: NewServiceListFormat("raw", false)}, - `id: id_baz -name: baz -mode: global -replicas: 2/4 -image: -ports: *:80->8080/tcp - -id: id_bar -name: bar -mode: replicated -replicas: 2/4 -image: -ports: *:80->8080/tcp - -`, - }, - { - Context{Format: NewServiceListFormat("raw", true)}, - `id: id_baz -id: id_bar -`, - }, - // Custom Format - { - Context{Format: NewServiceListFormat("{{.Name}}", false)}, - `baz -bar -`, - }, - } - - for _, testcase := range cases { - services := []swarm.Service{ - { - ID: "id_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - { - ID: "id_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ServiceListWrite(testcase.context, services, info) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestServiceContextWriteJSON(t *testing.T) { - services := []swarm.Service{ - { - ID: "id_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - { - ID: "id_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - expectedJSONs := []map[string]interface{}{ - {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, - {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, - } - - out := bytes.NewBufferString("") - err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} -func TestServiceContextWriteJSONField(t *testing.T) { - services := []swarm.Service{ - {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, - {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - out := bytes.NewBufferString("") - err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, services[i].Spec.Name, s) - } -} diff --git a/cli/command/formatter/stack.go b/cli/command/formatter/stack.go deleted file mode 100644 index 676bcc05fe..0000000000 --- a/cli/command/formatter/stack.go +++ /dev/null @@ -1,67 +0,0 @@ -package formatter - -import ( - "strconv" -) - -const ( - defaultStackTableFormat = "table {{.Name}}\t{{.Services}}" - - stackServicesHeader = "SERVICES" -) - -// Stack contains deployed stack information. -type Stack struct { - // Name is the name of the stack - Name string - // Services is the number of the services - Services int -} - -// NewStackFormat returns a format for use with a stack Context -func NewStackFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultStackTableFormat - } - return Format(source) -} - -// StackWrite writes formatted stacks using the Context -func StackWrite(ctx Context, stacks []*Stack) error { - render := func(format func(subContext subContext) error) error { - for _, stack := range stacks { - if err := format(&stackContext{s: stack}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newStackContext(), render) -} - -type stackContext struct { - HeaderContext - s *Stack -} - -func newStackContext() *stackContext { - stackCtx := stackContext{} - stackCtx.header = map[string]string{ - "Name": nameHeader, - "Services": stackServicesHeader, - } - return &stackCtx -} - -func (s *stackContext) MarshalJSON() ([]byte, error) { - return marshalJSON(s) -} - -func (s *stackContext) Name() string { - return s.s.Name -} - -func (s *stackContext) Services() string { - return strconv.Itoa(s.s.Services) -} diff --git a/cli/command/formatter/stack_test.go b/cli/command/formatter/stack_test.go deleted file mode 100644 index b18ae7f083..0000000000 --- a/cli/command/formatter/stack_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestStackContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewStackFormat("table")}, - `NAME SERVICES -baz 2 -bar 1 -`, - }, - { - Context{Format: NewStackFormat("table {{.Name}}")}, - `NAME -baz -bar -`, - }, - // Custom Format - { - Context{Format: NewStackFormat("{{.Name}}")}, - `baz -bar -`, - }, - } - - stacks := []*Stack{ - {Name: "baz", Services: 2}, - {Name: "bar", Services: 1}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := StackWrite(testcase.context, stacks) - if err != nil { - assert.Error(t, err, testcase.expected) - } else { - assert.Equal(t, out.String(), testcase.expected) - } - } -} diff --git a/cli/command/formatter/stats.go b/cli/command/formatter/stats.go deleted file mode 100644 index c0151101a0..0000000000 --- a/cli/command/formatter/stats.go +++ /dev/null @@ -1,220 +0,0 @@ -package formatter - -import ( - "fmt" - "sync" - - units "github.com/docker/go-units" -) - -const ( - winOSType = "windows" - defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" - winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" - - containerHeader = "CONTAINER" - cpuPercHeader = "CPU %" - netIOHeader = "NET I/O" - blockIOHeader = "BLOCK I/O" - memPercHeader = "MEM %" // Used only on Linux - winMemUseHeader = "PRIV WORKING SET" // Used only on Windows - memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux - pidsHeader = "PIDS" // Used only on Linux -) - -// StatsEntry represents represents the statistics data collected from a container -type StatsEntry struct { - Container string - Name string - ID string - CPUPercentage float64 - Memory float64 // On Windows this is the private working set - MemoryLimit float64 // Not used on Windows - MemoryPercentage float64 // Not used on Windows - NetworkRx float64 - NetworkTx float64 - BlockRead float64 - BlockWrite float64 - PidsCurrent uint64 // Not used on Windows - IsInvalid bool -} - -// ContainerStats represents an entity to store containers statistics synchronously -type ContainerStats struct { - mutex sync.Mutex - StatsEntry - err error -} - -// GetError returns the container statistics error. -// This is used to determine whether the statistics are valid or not -func (cs *ContainerStats) GetError() error { - cs.mutex.Lock() - defer cs.mutex.Unlock() - return cs.err -} - -// SetErrorAndReset zeroes all the container statistics and store the error. -// It is used when receiving time out error during statistics collecting to reduce lock overhead -func (cs *ContainerStats) SetErrorAndReset(err error) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - cs.CPUPercentage = 0 - cs.Memory = 0 - cs.MemoryPercentage = 0 - cs.MemoryLimit = 0 - cs.NetworkRx = 0 - cs.NetworkTx = 0 - cs.BlockRead = 0 - cs.BlockWrite = 0 - cs.PidsCurrent = 0 - cs.err = err - cs.IsInvalid = true -} - -// SetError sets container statistics error -func (cs *ContainerStats) SetError(err error) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - cs.err = err - if err != nil { - cs.IsInvalid = true - } -} - -// SetStatistics set the container statistics -func (cs *ContainerStats) SetStatistics(s StatsEntry) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - s.Container = cs.Container - cs.StatsEntry = s -} - -// GetStatistics returns container statistics with other meta data such as the container name -func (cs *ContainerStats) GetStatistics() StatsEntry { - cs.mutex.Lock() - defer cs.mutex.Unlock() - return cs.StatsEntry -} - -// NewStatsFormat returns a format for rendering an CStatsContext -func NewStatsFormat(source, osType string) Format { - if source == TableFormatKey { - if osType == winOSType { - return Format(winDefaultStatsTableFormat) - } - return Format(defaultStatsTableFormat) - } - return Format(source) -} - -// NewContainerStats returns a new ContainerStats entity and sets in it the given name -func NewContainerStats(container, osType string) *ContainerStats { - return &ContainerStats{ - StatsEntry: StatsEntry{Container: container}, - } -} - -// ContainerStatsWrite renders the context for a list of containers statistics -func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error { - render := func(format func(subContext subContext) error) error { - for _, cstats := range containerStats { - containerStatsCtx := &containerStatsContext{ - s: cstats, - os: osType, - } - if err := format(containerStatsCtx); err != nil { - return err - } - } - return nil - } - memUsage := memUseHeader - if osType == winOSType { - memUsage = winMemUseHeader - } - containerStatsCtx := containerStatsContext{} - containerStatsCtx.header = map[string]string{ - "Container": containerHeader, - "Name": nameHeader, - "ID": containerIDHeader, - "CPUPerc": cpuPercHeader, - "MemUsage": memUsage, - "MemPerc": memPercHeader, - "NetIO": netIOHeader, - "BlockIO": blockIOHeader, - "PIDs": pidsHeader, - } - containerStatsCtx.os = osType - return ctx.Write(&containerStatsCtx, render) -} - -type containerStatsContext struct { - HeaderContext - s StatsEntry - os string -} - -func (c *containerStatsContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *containerStatsContext) Container() string { - return c.s.Container -} - -func (c *containerStatsContext) Name() string { - if len(c.s.Name) > 1 { - return c.s.Name[1:] - } - return "--" -} - -func (c *containerStatsContext) ID() string { - return c.s.ID -} - -func (c *containerStatsContext) CPUPerc() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) -} - -func (c *containerStatsContext) MemUsage() string { - if c.s.IsInvalid { - return fmt.Sprintf("-- / --") - } - if c.os == winOSType { - return fmt.Sprintf("%s", units.BytesSize(c.s.Memory)) - } - return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) -} - -func (c *containerStatsContext) MemPerc() string { - if c.s.IsInvalid || c.os == winOSType { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) -} - -func (c *containerStatsContext) NetIO() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) -} - -func (c *containerStatsContext) BlockIO() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) -} - -func (c *containerStatsContext) PIDs() string { - if c.s.IsInvalid || c.os == winOSType { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%d", c.s.PidsCurrent) -} diff --git a/cli/command/formatter/stats_test.go b/cli/command/formatter/stats_test.go deleted file mode 100644 index 078e8db32a..0000000000 --- a/cli/command/formatter/stats_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestContainerStatsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() - - var ctx containerStatsContext - tt := []struct { - stats StatsEntry - osType string - expValue string - expHeader string - call func() string - }{ - {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, - {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, - {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage}, - {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, - } - - for _, te := range tt { - ctx = containerStatsContext{s: te.stats, os: te.osType} - if v := te.call(); v != te.expValue { - t.Fatalf("Expected %q, got %q", te.expValue, v) - } - } -} - -func TestContainerStatsContextWrite(t *testing.T) { - tt := []struct { - context Context - expected string - }{ - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - { - Context{Format: "table {{.MemUsage}}"}, - `MEM USAGE / LIMIT -20B / 20B --- / -- -`, - }, - { - Context{Format: "{{.Container}} {{.ID}} {{.Name}}"}, - `container1 abcdef foo -container2 -- -`, - }, - { - Context{Format: "{{.Container}} {{.CPUPerc}}"}, - `container1 20.00% -container2 -- -`, - }, - } - - for _, te := range tt { - stats := []StatsEntry{ - { - Container: "container1", - ID: "abcdef", - Name: "/foo", - CPUPercentage: 20, - Memory: 20, - MemoryLimit: 20, - MemoryPercentage: 20, - NetworkRx: 20, - NetworkTx: 20, - BlockRead: 20, - BlockWrite: 20, - PidsCurrent: 2, - IsInvalid: false, - }, - { - Container: "container2", - CPUPercentage: 30, - Memory: 30, - MemoryLimit: 30, - MemoryPercentage: 30, - NetworkRx: 30, - NetworkTx: 30, - BlockRead: 30, - BlockWrite: 30, - PidsCurrent: 3, - IsInvalid: true, - }, - } - var out bytes.Buffer - te.context.Output = &out - err := ContainerStatsWrite(te.context, stats, "linux") - if err != nil { - assert.EqualError(t, err, te.expected) - } else { - assert.Equal(t, te.expected, out.String()) - } - } -} - -func TestContainerStatsContextWriteWindows(t *testing.T) { - tt := []struct { - context Context - expected string - }{ - { - Context{Format: "table {{.MemUsage}}"}, - `PRIV WORKING SET -20B --- / -- -`, - }, - { - Context{Format: "{{.Container}} {{.CPUPerc}}"}, - `container1 20.00% -container2 -- -`, - }, - { - Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"}, - `container1 -- -- -container2 -- -- -`, - }, - } - - for _, te := range tt { - stats := []StatsEntry{ - { - Container: "container1", - CPUPercentage: 20, - Memory: 20, - MemoryLimit: 20, - MemoryPercentage: 20, - NetworkRx: 20, - NetworkTx: 20, - BlockRead: 20, - BlockWrite: 20, - PidsCurrent: 2, - IsInvalid: false, - }, - { - Container: "container2", - CPUPercentage: 30, - Memory: 30, - MemoryLimit: 30, - MemoryPercentage: 30, - NetworkRx: 30, - NetworkTx: 30, - BlockRead: 30, - BlockWrite: 30, - PidsCurrent: 3, - IsInvalid: true, - }, - } - var out bytes.Buffer - te.context.Output = &out - err := ContainerStatsWrite(te.context, stats, "windows") - if err != nil { - assert.EqualError(t, err, te.expected) - } else { - assert.Equal(t, te.expected, out.String()) - } - } -} - -func TestContainerStatsContextWriteWithNoStats(t *testing.T) { - var out bytes.Buffer - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Container}}", - Output: &out, - }, - "", - }, - { - Context{ - Format: "table {{.Container}}", - Output: &out, - }, - "CONTAINER\n", - }, - { - Context{ - Format: "table {{.Container}}\t{{.CPUPerc}}", - Output: &out, - }, - "CONTAINER CPU %\n", - }, - } - - for _, context := range contexts { - ContainerStatsWrite(context.context, []StatsEntry{}, "linux") - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} - -func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) { - var out bytes.Buffer - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Container}}", - Output: &out, - }, - "", - }, - { - Context{ - Format: "table {{.Container}}\t{{.MemUsage}}", - Output: &out, - }, - "CONTAINER PRIV WORKING SET\n", - }, - { - Context{ - Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", - Output: &out, - }, - "CONTAINER CPU % PRIV WORKING SET\n", - }, - } - - for _, context := range contexts { - ContainerStatsWrite(context.context, []StatsEntry{}, "windows") - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/task.go b/cli/command/formatter/task.go deleted file mode 100644 index 2c6e7bb124..0000000000 --- a/cli/command/formatter/task.go +++ /dev/null @@ -1,150 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/go-units" -) - -const ( - defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}" - - nodeHeader = "NODE" - taskIDHeader = "ID" - desiredStateHeader = "DESIRED STATE" - currentStateHeader = "CURRENT STATE" - errorHeader = "ERROR" - - maxErrLength = 30 -) - -// NewTaskFormat returns a Format for rendering using a task Context -func NewTaskFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultTaskTableFormat - case RawFormatKey: - if quiet { - return `id: {{.ID}}` - } - return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n` - } - return Format(source) -} - -// TaskWrite writes the context -func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { - render := func(format func(subContext subContext) error) error { - for _, task := range tasks { - taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} - if err := format(taskCtx); err != nil { - return err - } - } - return nil - } - taskCtx := taskContext{} - taskCtx.header = taskHeaderContext{ - "ID": taskIDHeader, - "Name": nameHeader, - "Image": imageHeader, - "Node": nodeHeader, - "DesiredState": desiredStateHeader, - "CurrentState": currentStateHeader, - "Error": errorHeader, - "Ports": portsHeader, - } - return ctx.Write(&taskCtx, render) -} - -type taskHeaderContext map[string]string - -type taskContext struct { - HeaderContext - trunc bool - task swarm.Task - name string - node string -} - -func (c *taskContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *taskContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.task.ID) - } - return c.task.ID -} - -func (c *taskContext) Name() string { - return c.name -} - -func (c *taskContext) Image() string { - image := c.task.Spec.ContainerSpec.Image - if c.trunc { - ref, err := reference.ParseNormalizedNamed(image) - if err == nil { - // update image string for display, (strips any digest) - if nt, ok := ref.(reference.NamedTagged); ok { - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - image = reference.FamiliarString(namedTagged) - } - } - } - } - return image -} - -func (c *taskContext) Node() string { - return c.node -} - -func (c *taskContext) DesiredState() string { - return command.PrettyPrint(c.task.DesiredState) -} - -func (c *taskContext) CurrentState() string { - return fmt.Sprintf("%s %s ago", - command.PrettyPrint(c.task.Status.State), - strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))), - ) -} - -func (c *taskContext) Error() string { - // Trim and quote the error message. - taskErr := c.task.Status.Err - if c.trunc && len(taskErr) > maxErrLength { - taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) - } - if len(taskErr) > 0 { - taskErr = fmt.Sprintf("\"%s\"", taskErr) - } - return taskErr -} - -func (c *taskContext) Ports() string { - if len(c.task.Status.PortStatus.Ports) == 0 { - return "" - } - ports := []string{} - for _, pConfig := range c.task.Status.PortStatus.Ports { - ports = append(ports, fmt.Sprintf("*:%d->%d/%s", - pConfig.PublishedPort, - pConfig.TargetPort, - pConfig.Protocol, - )) - } - return strings.Join(ports, ",") -} diff --git a/cli/command/formatter/task_test.go b/cli/command/formatter/task_test.go deleted file mode 100644 index d2843c70d4..0000000000 --- a/cli/command/formatter/task_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestTaskContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - { - Context{Format: NewTaskFormat("table", true)}, - `taskID1 -taskID2 -`, - }, - { - Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, - `NAME NODE PORTS -foobar_baz foo1 -foobar_bar foo2 -`, - }, - { - Context{Format: NewTaskFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewTaskFormat("raw", true)}, - `id: taskID1 -id: taskID2 -`, - }, - { - Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, - `foobar_baz foo1 -foobar_bar foo2 -`, - }, - } - - for _, testcase := range cases { - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, - } - names := map[string]string{ - "taskID1": "foobar_baz", - "taskID2": "foobar_bar", - } - nodes := map[string]string{ - "taskID1": "foo1", - "taskID2": "foo2", - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := TaskWrite(testcase.context, tasks, names, nodes) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestTaskContextWriteJSONField(t *testing.T) { - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, - } - names := map[string]string{ - "taskID1": "foobar_baz", - "taskID2": "foobar_bar", - } - out := bytes.NewBufferString("") - err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, tasks[i].ID, s) - } -} diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go deleted file mode 100644 index 342f2fb934..0000000000 --- a/cli/command/formatter/volume.go +++ /dev/null @@ -1,131 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - units "github.com/docker/go-units" -) - -const ( - defaultVolumeQuietFormat = "{{.Name}}" - defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" - - volumeNameHeader = "VOLUME NAME" - mountpointHeader = "MOUNTPOINT" - linksHeader = "LINKS" - // Status header ? -) - -// NewVolumeFormat returns a format for use with a volume Context -func NewVolumeFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultVolumeQuietFormat - } - return defaultVolumeTableFormat - case RawFormatKey: - if quiet { - return `name: {{.Name}}` - } - return `name: {{.Name}}\ndriver: {{.Driver}}\n` - } - return Format(source) -} - -// VolumeWrite writes formatted volumes using the Context -func VolumeWrite(ctx Context, volumes []*types.Volume) error { - render := func(format func(subContext subContext) error) error { - for _, volume := range volumes { - if err := format(&volumeContext{v: *volume}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newVolumeContext(), render) -} - -type volumeHeaderContext map[string]string - -func (c volumeHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type volumeContext struct { - HeaderContext - v types.Volume -} - -func newVolumeContext() *volumeContext { - volumeCtx := volumeContext{} - volumeCtx.header = volumeHeaderContext{ - "Name": volumeNameHeader, - "Driver": driverHeader, - "Scope": scopeHeader, - "Mountpoint": mountpointHeader, - "Labels": labelsHeader, - "Links": linksHeader, - "Size": sizeHeader, - } - return &volumeCtx -} - -func (c *volumeContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *volumeContext) Name() string { - return c.v.Name -} - -func (c *volumeContext) Driver() string { - return c.v.Driver -} - -func (c *volumeContext) Scope() string { - return c.v.Scope -} - -func (c *volumeContext) Mountpoint() string { - return c.v.Mountpoint -} - -func (c *volumeContext) Labels() string { - if c.v.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.v.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *volumeContext) Label(name string) string { - if c.v.Labels == nil { - return "" - } - return c.v.Labels[name] -} - -func (c *volumeContext) Links() string { - if c.v.UsageData == nil { - return "N/A" - } - return fmt.Sprintf("%d", c.v.UsageData.RefCount) -} - -func (c *volumeContext) Size() string { - if c.v.UsageData == nil { - return "N/A" - } - return units.HumanSize(float64(c.v.UsageData.Size)) -} diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go deleted file mode 100644 index bf1100893f..0000000000 --- a/cli/command/formatter/volume_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestVolumeContext(t *testing.T) { - volumeName := stringid.GenerateRandomID() - - var ctx volumeContext - cases := []struct { - volumeCtx volumeContext - expValue string - call func() string - }{ - {volumeContext{ - v: types.Volume{Name: volumeName}, - }, volumeName, ctx.Name}, - {volumeContext{ - v: types.Volume{Driver: "driver_name"}, - }, "driver_name", ctx.Driver}, - {volumeContext{ - v: types.Volume{Scope: "local"}, - }, "local", ctx.Scope}, - {volumeContext{ - v: types.Volume{Mountpoint: "mountpoint"}, - }, "mountpoint", ctx.Mountpoint}, - {volumeContext{ - v: types.Volume{}, - }, "", ctx.Labels}, - {volumeContext{ - v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", ctx.Labels}, - } - - for _, c := range cases { - ctx = c.volumeCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestVolumeContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewVolumeFormat("table", false)}, - `DRIVER VOLUME NAME -foo foobar_baz -bar foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table", true)}, - `foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table {{.Name}}", false)}, - `VOLUME NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table {{.Name}}", true)}, - `VOLUME NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewVolumeFormat("raw", false)}, - `name: foobar_baz -driver: foo - -name: foobar_bar -driver: bar - -`, - }, - { - Context{Format: NewVolumeFormat("raw", true)}, - `name: foobar_baz -name: foobar_bar -`, - }, - // Custom Format - { - Context{Format: NewVolumeFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - volumes := []*types.Volume{ - {Name: "foobar_baz", Driver: "foo"}, - {Name: "foobar_bar", Driver: "bar"}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := VolumeWrite(testcase.context, volumes) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestVolumeContextWriteJSON(t *testing.T) { - volumes := []*types.Volume{ - {Driver: "foo", Name: "foobar_baz"}, - {Driver: "bar", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"}, - {"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"}, - } - out := bytes.NewBufferString("") - err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestVolumeContextWriteJSONField(t *testing.T) { - volumes := []*types.Volume{ - {Driver: "foo", Name: "foobar_baz"}, - {Driver: "bar", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, volumes[i].Name, s) - } -} diff --git a/cli/command/idresolver/client_test.go b/cli/command/idresolver/client_test.go deleted file mode 100644 index f84683b907..0000000000 --- a/cli/command/idresolver/client_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package idresolver - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - nodeInspectFunc func(string) (swarm.Node, []byte, error) - serviceInspectFunc func(string) (swarm.Service, []byte, error) -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc(nodeID) - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { - if cli.serviceInspectFunc != nil { - return cli.serviceInspectFunc(serviceID) - } - return swarm.Service{}, []byte{}, nil -} diff --git a/cli/command/idresolver/idresolver.go b/cli/command/idresolver/idresolver.go deleted file mode 100644 index 6088b64b59..0000000000 --- a/cli/command/idresolver/idresolver.go +++ /dev/null @@ -1,70 +0,0 @@ -package idresolver - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" -) - -// IDResolver provides ID to Name resolution. -type IDResolver struct { - client client.APIClient - noResolve bool - cache map[string]string -} - -// New creates a new IDResolver. -func New(client client.APIClient, noResolve bool) *IDResolver { - return &IDResolver{ - client: client, - noResolve: noResolve, - cache: make(map[string]string), - } -} - -func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) { - switch t.(type) { - case swarm.Node: - node, _, err := r.client.NodeInspectWithRaw(ctx, id) - if err != nil { - return id, nil - } - if node.Spec.Annotations.Name != "" { - return node.Spec.Annotations.Name, nil - } - if node.Description.Hostname != "" { - return node.Description.Hostname, nil - } - return id, nil - case swarm.Service: - service, _, err := r.client.ServiceInspectWithRaw(ctx, id, types.ServiceInspectOptions{}) - if err != nil { - return id, nil - } - return service.Spec.Annotations.Name, nil - default: - return "", errors.Errorf("unsupported type") - } - -} - -// Resolve will attempt to resolve an ID to a Name by querying the manager. -// Results are stored into a cache. -// If the `-n` flag is used in the command-line, resolution is disabled. -func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) { - if r.noResolve { - return id, nil - } - if name, ok := r.cache[id]; ok { - return name, nil - } - name, err := r.get(ctx, t, id) - if err != nil { - return "", err - } - r.cache[id] = name - return name, nil -} diff --git a/cli/command/idresolver/idresolver_test.go b/cli/command/idresolver/idresolver_test.go deleted file mode 100644 index 1aca09ce96..0000000000 --- a/cli/command/idresolver/idresolver_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package idresolver - -import ( - "testing" - - "github.com/docker/docker/api/types/swarm" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestResolveError(t *testing.T) { - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - } - - idResolver := New(cli, false) - _, err := idResolver.Resolve(context.Background(), struct{}{}, "nodeID") - - assert.EqualError(t, err, "unsupported type") -} - -func TestResolveWithNoResolveOption(t *testing.T) { - resolved := false - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - resolved = true - return swarm.Node{}, []byte{}, nil - }, - serviceInspectFunc: func(serviceID string) (swarm.Service, []byte, error) { - resolved = true - return swarm.Service{}, []byte{}, nil - }, - } - - idResolver := New(cli, true) - id, err := idResolver.Resolve(context.Background(), swarm.Node{}, "nodeID") - - assert.NoError(t, err) - assert.Equal(t, "nodeID", id) - assert.False(t, resolved) -} - -func TestResolveWithCache(t *testing.T) { - inspectCounter := 0 - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - inspectCounter++ - return *Node(NodeName("node-foo")), []byte{}, nil - }, - } - - idResolver := New(cli, false) - - ctx := context.Background() - for i := 0; i < 2; i++ { - id, err := idResolver.Resolve(ctx, swarm.Node{}, "nodeID") - assert.NoError(t, err) - assert.Equal(t, "node-foo", id) - } - - assert.Equal(t, 1, inspectCounter) -} - -func TestResolveNode(t *testing.T) { - testCases := []struct { - nodeID string - nodeInspectFunc func(string) (swarm.Node, []byte, error) - expectedID string - }{ - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - expectedID: "nodeID", - }, - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *Node(NodeName("node-foo")), []byte{}, nil - }, - expectedID: "node-foo", - }, - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *Node(NodeName(""), Hostname("node-hostname")), []byte{}, nil - }, - expectedID: "node-hostname", - }, - } - - ctx := context.Background() - for _, tc := range testCases { - cli := &fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - } - idResolver := New(cli, false) - id, err := idResolver.Resolve(ctx, swarm.Node{}, tc.nodeID) - - assert.NoError(t, err) - assert.Equal(t, tc.expectedID, id) - } -} - -func TestResolveService(t *testing.T) { - testCases := []struct { - serviceID string - serviceInspectFunc func(string) (swarm.Service, []byte, error) - expectedID string - }{ - { - serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return swarm.Service{}, []byte{}, errors.Errorf("error inspecting service") - }, - expectedID: "serviceID", - }, - { - serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return *Service(ServiceName("service-foo")), []byte{}, nil - }, - expectedID: "service-foo", - }, - } - - ctx := context.Background() - for _, tc := range testCases { - cli := &fakeClient{ - serviceInspectFunc: tc.serviceInspectFunc, - } - idResolver := New(cli, false) - id, err := idResolver.Resolve(ctx, swarm.Service{}, tc.serviceID) - - assert.NoError(t, err) - assert.Equal(t, tc.expectedID, id) - } -} diff --git a/cli/command/image/build.go b/cli/command/image/build.go deleted file mode 100644 index de12afdad3..0000000000 --- a/cli/command/image/build.go +++ /dev/null @@ -1,558 +0,0 @@ -package image - -import ( - "archive/tar" - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "runtime" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/builder/dockerignore" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image/build" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/fileutils" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/urlutil" - runconfigopts "github.com/docker/docker/runconfig/opts" - units "github.com/docker/go-units" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type buildOptions struct { - context string - dockerfileName string - tags opts.ListOpts - labels opts.ListOpts - buildArgs opts.ListOpts - extraHosts opts.ListOpts - ulimits *opts.UlimitOpt - memory opts.MemBytes - memorySwap opts.MemSwapBytes - shmSize opts.MemBytes - cpuShares int64 - cpuPeriod int64 - cpuQuota int64 - cpuSetCpus string - cpuSetMems string - cgroupParent string - isolation string - quiet bool - noCache bool - rm bool - forceRm bool - pull bool - cacheFrom []string - compress bool - securityOpt []string - networkMode string - squash bool - target string - imageIDFile string -} - -// NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { - ulimits := make(map[string]*units.Ulimit) - options := buildOptions{ - tags: opts.NewListOpts(validateTag), - buildArgs: opts.NewListOpts(opts.ValidateEnv), - ulimits: opts.NewUlimitOpt(&ulimits), - labels: opts.NewListOpts(opts.ValidateEnv), - extraHosts: opts.NewListOpts(opts.ValidateExtraHost), - } - - cmd := &cobra.Command{ - Use: "build [OPTIONS] PATH | URL | -", - Short: "Build an image from a Dockerfile", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.context = args[0] - return runBuild(dockerCli, options) - }, - } - - flags := cmd.Flags() - - flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") - flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") - flags.Var(options.ulimits, "ulimit", "Ulimit options") - flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") - flags.VarP(&options.memory, "memory", "m", "Memory limit") - flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") - flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") - flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") - flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") - flags.Var(&options.labels, "label", "Set metadata for an image") - flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") - flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") - flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") - 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.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") - 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.SetAnnotation("network", "version", []string{"1.25"}) - flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") - flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") - flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") - - command.AddTrustVerificationFlags(flags) - - flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") - flags.SetAnnotation("squash", "experimental", nil) - flags.SetAnnotation("squash", "version", []string{"1.25"}) - - return cmd -} - -// lastProgressOutput is the same as progress.Output except -// that it only output with the last update. It is used in -// non terminal scenarios to suppress verbose messages -type lastProgressOutput struct { - output progress.Output -} - -// WriteProgress formats progress information from a ProgressReader. -func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { - if !prog.LastUpdate { - return nil - } - - return out.output.WriteProgress(prog) -} - -func runBuild(dockerCli *command.DockerCli, options buildOptions) error { - var ( - buildCtx io.ReadCloser - dockerfileCtx io.ReadCloser - err error - contextDir string - tempDir string - relDockerfile string - progBuff io.Writer - buildBuff io.Writer - ) - - specifiedContext := options.context - progBuff = dockerCli.Out() - buildBuff = dockerCli.Out() - if options.quiet { - progBuff = bytes.NewBuffer(nil) - buildBuff = bytes.NewBuffer(nil) - } - if options.imageIDFile != "" { - // Avoid leaving a stale file if we eventually fail - if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) { - return errors.Wrap(err, "Removing image ID file") - } - } - - if options.dockerfileName == "-" { - if specifiedContext == "-" { - return errors.New("invalid argument: can't use stdin for both build context and dockerfile") - } - dockerfileCtx = dockerCli.In() - } - - switch { - case specifiedContext == "-": - buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) - case isLocalDir(specifiedContext): - contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) - case urlutil.IsGitURL(specifiedContext): - tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) - case urlutil.IsURL(specifiedContext): - buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) - default: - return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) - } - - if err != nil { - if options.quiet && urlutil.IsURL(specifiedContext) { - fmt.Fprintln(dockerCli.Err(), progBuff) - } - return errors.Errorf("unable to prepare context: %s", err) - } - - if tempDir != "" { - defer os.RemoveAll(tempDir) - contextDir = tempDir - } - - if buildCtx == nil { - // And canonicalize dockerfile name to a platform-independent one - relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) - if err != nil { - return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) - } - - f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) - if err != nil && !os.IsNotExist(err) { - return err - } - defer f.Close() - - var excludes []string - if err == nil { - excludes, err = dockerignore.ReadAll(f) - if err != nil { - return err - } - } - - if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { - return errors.Errorf("Error checking context: '%s'.", err) - } - - // If .dockerignore mentions .dockerignore or the Dockerfile then make - // sure we send both files over to the daemon because Dockerfile is, - // obviously, needed no matter what, and .dockerignore is needed to know - // if either one needs to be removed. The daemon will remove them - // if necessary, after it parses the Dockerfile. Ignore errors here, as - // they will have been caught by validateContextDirectory above. - // Excludes are used instead of includes to maintain the order of files - // in the archive. - if keep, _ := fileutils.Matches(".dockerignore", excludes); keep { - excludes = append(excludes, "!.dockerignore") - } - if keep, _ := fileutils.Matches(relDockerfile, excludes); keep && dockerfileCtx == nil { - excludes = append(excludes, "!"+relDockerfile) - } - - compression := archive.Uncompressed - if options.compress { - compression = archive.Gzip - } - buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ - Compression: compression, - ExcludePatterns: excludes, - }) - if err != nil { - return err - } - } - - // replace Dockerfile if added dynamically - if dockerfileCtx != nil { - buildCtx, relDockerfile, err = addDockerfileToBuildContext(dockerfileCtx, buildCtx) - if err != nil { - return err - } - } - - ctx := context.Background() - - var resolvedTags []*resolvedTag - if command.IsTrusted() { - translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref, nil) - } - // Wrap the tar archive to replace the Dockerfile entry with the rewritten - // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) - } - - // Setup an upload progress bar - progressOutput := streamformatter.NewProgressOutput(progBuff) - if !dockerCli.Out().IsTerminal() { - progressOutput = &lastProgressOutput{output: progressOutput} - } - - var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") - - authConfigs, _ := dockerCli.GetAllCredentials() - buildOptions := types.ImageBuildOptions{ - Memory: options.memory.Value(), - MemorySwap: options.memorySwap.Value(), - Tags: options.tags.GetAll(), - SuppressOutput: options.quiet, - NoCache: options.noCache, - Remove: options.rm, - ForceRemove: options.forceRm, - PullParent: options.pull, - Isolation: container.Isolation(options.isolation), - CPUSetCPUs: options.cpuSetCpus, - CPUSetMems: options.cpuSetMems, - CPUShares: options.cpuShares, - CPUQuota: options.cpuQuota, - CPUPeriod: options.cpuPeriod, - CgroupParent: options.cgroupParent, - Dockerfile: relDockerfile, - ShmSize: options.shmSize.Value(), - Ulimits: options.ulimits.GetList(), - BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), - AuthConfigs: authConfigs, - Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), - CacheFrom: options.cacheFrom, - SecurityOpt: options.securityOpt, - NetworkMode: options.networkMode, - Squash: options.squash, - ExtraHosts: options.extraHosts.GetAll(), - Target: options.target, - } - - response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) - if err != nil { - if options.quiet { - fmt.Fprintf(dockerCli.Err(), "%s", progBuff) - } - return err - } - defer response.Body.Close() - - imageID := "" - aux := func(auxJSON *json.RawMessage) { - var result types.BuildResult - if err := json.Unmarshal(*auxJSON, &result); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err) - } else { - imageID = result.ID - } - } - - err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux) - if err != nil { - if jerr, ok := err.(*jsonmessage.JSONError); ok { - // If no error code is set, default to 1 - if jerr.Code == 0 { - jerr.Code = 1 - } - if options.quiet { - fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) - } - return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} - } - return err - } - - // Windows: show error message about modified file permissions if the - // daemon isn't running Windows. - if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ - "image from Windows against a non-Windows Docker host. All files and "+ - "directories added to build context will have '-rwxr-xr-x' permissions. "+ - "It is recommended to double check and reset permissions for sensitive "+ - "files and directories.") - } - - // Everything worked so if -q was provided the output from the daemon - // should be just the image ID and we'll print that to stdout. - if options.quiet { - imageID = fmt.Sprintf("%s", buildBuff) - fmt.Fprintf(dockerCli.Out(), imageID) - } - - if options.imageIDFile != "" { - if imageID == "" { - return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile) - } - if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil { - return err - } - } - if command.IsTrusted() { - // Since the build was successful, now we must tag any of the resolved - // images from the above Dockerfile rewrite. - for _, resolved := range resolvedTags { - if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { - return err - } - } - } - - return nil -} - -func addDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { - file, err := ioutil.ReadAll(dockerfileCtx) - dockerfileCtx.Close() - if err != nil { - return nil, "", err - } - now := time.Now() - hdrTmpl := &tar.Header{ - Mode: 0600, - Uid: 0, - Gid: 0, - ModTime: now, - Typeflag: tar.TypeReg, - AccessTime: now, - ChangeTime: now, - } - randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] - - buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ - // Add the dockerfile with a random filename - randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - return hdrTmpl, file, nil - }, - // Update .dockerignore to include the random filename - ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - if h == nil { - h = hdrTmpl - } - - b := &bytes.Buffer{} - if content != nil { - if _, err := b.ReadFrom(content); err != nil { - return nil, nil, err - } - } else { - b.WriteString(".dockerignore") - } - b.WriteString("\n" + randomName + "\n") - return h, b.Bytes(), nil - }, - }) - return buildCtx, randomName, nil -} - -func isLocalDir(c string) bool { - _, err := os.Stat(c) - return err == nil -} - -type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) - -// validateTag checks if the given image name can be resolved. -func validateTag(rawRepo string) (string, error) { - _, err := reference.ParseNormalizedNamed(rawRepo) - if err != nil { - return "", err - } - - return rawRepo, nil -} - -var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) - -// resolvedTag records the repository, tag, and resolved digest reference -// from a Dockerfile rewrite. -type resolvedTag struct { - digestRef reference.Canonical - tagRef reference.NamedTagged -} - -// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in -// "FROM " instructions to a digest reference. `translator` is a -// function that takes a repository name and tag reference and returns a -// trusted digest reference. -func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { - scanner := bufio.NewScanner(dockerfile) - buf := bytes.NewBuffer(nil) - - // Scan the lines of the Dockerfile, looking for a "FROM" line. - for scanner.Scan() { - line := scanner.Text() - - matches := dockerfileFromLinePattern.FindStringSubmatch(line) - if matches != nil && matches[1] != api.NoBaseImageSpecifier { - // Replace the line with a resolved "FROM repo@digest" - var ref reference.Named - ref, err = reference.ParseNormalizedNamed(matches[1]) - if err != nil { - return nil, nil, err - } - ref = reference.TagNameOnly(ref) - if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { - trustedRef, err := translator(ctx, ref) - if err != nil { - return nil, nil, err - } - - line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef))) - resolvedTags = append(resolvedTags, &resolvedTag{ - digestRef: trustedRef, - tagRef: ref, - }) - } - } - - _, err := fmt.Fprintln(buf, line) - if err != nil { - return nil, nil, err - } - } - - return buf.Bytes(), resolvedTags, scanner.Err() -} - -// replaceDockerfileTarWrapper wraps the given input tar archive stream and -// replaces the entry with the given Dockerfile name with the contents of the -// new Dockerfile. Returns a new tar archive stream with the replaced -// Dockerfile. -func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { - pipeReader, pipeWriter := io.Pipe() - go func() { - tarReader := tar.NewReader(inputTarStream) - tarWriter := tar.NewWriter(pipeWriter) - - defer inputTarStream.Close() - - for { - hdr, err := tarReader.Next() - if err == io.EOF { - // Signals end of archive. - tarWriter.Close() - pipeWriter.Close() - return - } - if err != nil { - pipeWriter.CloseWithError(err) - return - } - - content := io.Reader(tarReader) - if hdr.Name == dockerfileName { - // This entry is the Dockerfile. Since the tar archive was - // generated from a directory on the local filesystem, the - // Dockerfile will only appear once in the archive. - var newDockerfile []byte - newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) - if err != nil { - pipeWriter.CloseWithError(err) - return - } - hdr.Size = int64(len(newDockerfile)) - content = bytes.NewBuffer(newDockerfile) - } - - if err := tarWriter.WriteHeader(hdr); err != nil { - pipeWriter.CloseWithError(err) - return - } - - if _, err := io.Copy(tarWriter, content); err != nil { - pipeWriter.CloseWithError(err) - return - } - } - }() - - return pipeReader -} diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go deleted file mode 100644 index 5e66717a04..0000000000 --- a/cli/command/image/build/context.go +++ /dev/null @@ -1,275 +0,0 @@ -package build - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/fileutils" - "github.com/docker/docker/pkg/gitutils" - "github.com/docker/docker/pkg/httputils" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/pkg/errors" -) - -const ( - // DefaultDockerfileName is the Default filename with Docker commands, read by docker build - DefaultDockerfileName string = "Dockerfile" -) - -// ValidateContextDirectory checks if all the contents of the directory -// can be read and returns an error if some files can't be read -// symlinks which point to non-existing files don't trigger an error -func ValidateContextDirectory(srcPath string, excludes []string) error { - contextRoot, err := getContextRoot(srcPath) - if err != nil { - return err - } - return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { - if err != nil { - if os.IsPermission(err) { - return errors.Errorf("can't stat '%s'", filePath) - } - if os.IsNotExist(err) { - return nil - } - return err - } - - // skip this directory/file if it's not in the path, it won't get added to the context - if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { - return err - } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { - return err - } else if skip { - if f.IsDir() { - return filepath.SkipDir - } - return nil - } - - // skip checking if symlinks point to non-existing files, such symlinks can be useful - // also skip named pipes, because they hanging on open - if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { - return nil - } - - if !f.IsDir() { - currentFile, err := os.Open(filePath) - if err != nil && os.IsPermission(err) { - return errors.Errorf("no permission to read from '%s'", filePath) - } - currentFile.Close() - } - return nil - }) -} - -// GetContextFromReader will read the contents of the given reader as either a -// Dockerfile or tar archive. Returns a tar archive used as a context and a -// path to the Dockerfile inside the tar. -func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { - buf := bufio.NewReader(r) - - magic, err := buf.Peek(archive.HeaderSize) - if err != nil && err != io.EOF { - return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err) - } - - if archive.IsArchive(magic) { - return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil - } - - if dockerfileName == "-" { - return nil, "", errors.New("build context is not an archive") - } - - // Input should be read as a Dockerfile. - tmpDir, err := ioutil.TempDir("", "docker-build-context-") - if err != nil { - return nil, "", errors.Errorf("unable to create temporary context directory: %v", err) - } - - f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) - if err != nil { - return nil, "", err - } - _, err = io.Copy(f, buf) - if err != nil { - f.Close() - return nil, "", err - } - - if err := f.Close(); err != nil { - return nil, "", err - } - if err := r.Close(); err != nil { - return nil, "", err - } - - tar, err := archive.Tar(tmpDir, archive.Uncompressed) - if err != nil { - return nil, "", err - } - - return ioutils.NewReadCloserWrapper(tar, func() error { - err := tar.Close() - os.RemoveAll(tmpDir) - return err - }), DefaultDockerfileName, nil - -} - -// GetContextFromGitURL uses a Git URL as context for a `docker build`. The -// git repo is cloned into a temporary directory used as the context directory. -// Returns the absolute path to the temporary context directory, the relative -// path of the dockerfile in that context directory, and a non-nil error on -// success. -func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { - if _, err := exec.LookPath("git"); err != nil { - return "", "", errors.Errorf("unable to find 'git': %v", err) - } - if absContextDir, err = gitutils.Clone(gitURL); err != nil { - return "", "", errors.Errorf("unable to 'git clone' to temporary context directory: %v", err) - } - - return getDockerfileRelPath(absContextDir, dockerfileName) -} - -// GetContextFromURL uses a remote URL as context for a `docker build`. The -// remote resource is downloaded as either a Dockerfile or a tar archive. -// Returns the tar archive used for the context and a path of the -// dockerfile inside the tar. -func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { - response, err := httputils.Download(remoteURL) - if err != nil { - return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) - } - progressOutput := streamformatter.NewProgressOutput(out) - - // Pass the response body through a progress reader. - progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) - - return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) -} - -// GetContextFromLocalDir uses the given local directory as context for a -// `docker build`. Returns the absolute path to the local context directory, -// the relative path of the dockerfile in that context directory, and a non-nil -// error on success. -func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) { - // When using a local context directory, when the Dockerfile is specified - // with the `-f/--file` option then it is considered relative to the - // current directory and not the context directory. - if dockerfileName != "" && dockerfileName != "-" { - if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { - return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) - } - } - - return getDockerfileRelPath(localDir, dockerfileName) -} - -// getDockerfileRelPath uses the given context directory for a `docker build` -// and returns the absolute path to the context directory, the relative path of -// the dockerfile in that context directory, and a non-nil error on success. -func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { - if absContextDir, err = filepath.Abs(givenContextDir); err != nil { - return "", "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) - } - - // The context dir might be a symbolic link, so follow it to the actual - // target directory. - // - // FIXME. We use isUNC (always false on non-Windows platforms) to workaround - // an issue in golang. On Windows, EvalSymLinks does not work on UNC file - // paths (those starting with \\). This hack means that when using links - // on UNC paths, they will not be followed. - if !isUNC(absContextDir) { - absContextDir, err = filepath.EvalSymlinks(absContextDir) - if err != nil { - return "", "", errors.Errorf("unable to evaluate symlinks in context path: %v", err) - } - } - - stat, err := os.Lstat(absContextDir) - if err != nil { - return "", "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err) - } - - if !stat.IsDir() { - return "", "", errors.Errorf("context must be a directory: %s", absContextDir) - } - - absDockerfile := givenDockerfile - if absDockerfile == "" { - // No -f/--file was specified so use the default relative to the - // context directory. - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) - - // Just to be nice ;-) look for 'dockerfile' too but only - // use it if we found it, otherwise ignore this check - if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { - altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) - if _, err = os.Lstat(altPath); err == nil { - absDockerfile = altPath - } - } - } else if absDockerfile == "-" { - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) - } - - // If not already an absolute path, the Dockerfile path should be joined to - // the base directory. - if !filepath.IsAbs(absDockerfile) { - absDockerfile = filepath.Join(absContextDir, absDockerfile) - } - - // Evaluate symlinks in the path to the Dockerfile too. - // - // FIXME. We use isUNC (always false on non-Windows platforms) to workaround - // an issue in golang. On Windows, EvalSymLinks does not work on UNC file - // paths (those starting with \\). This hack means that when using links - // on UNC paths, they will not be followed. - if givenDockerfile != "-" { - if !isUNC(absDockerfile) { - absDockerfile, err = filepath.EvalSymlinks(absDockerfile) - if err != nil { - return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) - - } - } - - if _, err := os.Lstat(absDockerfile); err != nil { - if os.IsNotExist(err) { - return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) - } - return "", "", errors.Errorf("unable to stat Dockerfile: %v", err) - } - } - - if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { - return "", "", errors.Errorf("unable to get relative Dockerfile path: %v", err) - } - - if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - return "", "", errors.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) - } - - return absContextDir, relDockerfile, nil -} - -// isUNC returns true if the path is UNC (one starting \\). It always returns -// false on Linux. -func isUNC(path string) bool { - return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) -} diff --git a/cli/command/image/build/context_test.go b/cli/command/image/build/context_test.go deleted file mode 100644 index afa04a4fcd..0000000000 --- a/cli/command/image/build/context_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package build - -import ( - "archive/tar" - "bytes" - "io" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/docker/docker/pkg/archive" -) - -const dockerfileContents = "FROM busybox" - -var prepareEmpty = func(t *testing.T) (string, func()) { - return "", func() {} -} - -var prepareNoFiles = func(t *testing.T) (string, func()) { - return createTestTempDir(t, "", "builder-context-test") -} - -var prepareOneFile = func(t *testing.T) (string, func()) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - return contextDir, cleanup -} - -func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) (string, func()), excludes []string) { - contextDir, cleanup := prepare(t) - defer cleanup() - - err := ValidateContextDirectory(contextDir, excludes) - - if err != nil { - t.Fatalf("Error should be nil, got: %s", err) - } -} - -func TestGetContextFromLocalDirNoDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirNotExistingDir(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - fakePath := filepath.Join(contextDir, "fake") - - absContextDir, relDockerfile, err := GetContextFromLocalDir(fakePath, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - fakePath := filepath.Join(contextDir, "fake") - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, fakePath) - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { - contextDir, dirCleanup := createTestTempDir(t, "", "builder-context-test") - defer dirCleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - chdirCleanup := chdir(t, contextDir) - defer chdirCleanup() - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromLocalDirLocalFile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - testFilename := createTestTempFile(t, contextDir, "tmpTest", "test", 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - chdirCleanup := chdir(t, contextDir) - defer chdirCleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } - -} - -func TestGetContextFromReaderString(t *testing.T) { - tarArchive, relDockerfile, err := GetContextFromReader(ioutil.NopCloser(strings.NewReader(dockerfileContents)), "") - - if err != nil { - t.Fatalf("Error when executing GetContextFromReader: %s", err) - } - - tarReader := tar.NewReader(tarArchive) - - _, err = tarReader.Next() - - if err != nil { - t.Fatalf("Error when reading tar archive: %s", err) - } - - buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) - contents := buff.String() - - _, err = tarReader.Next() - - if err != io.EOF { - t.Fatalf("Tar stream too long: %s", err) - } - - if err = tarArchive.Close(); err != nil { - t.Fatalf("Error when closing tar stream: %s", err) - } - - if dockerfileContents != contents { - t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromReaderTar(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - tarStream, err := archive.Tar(contextDir, archive.Uncompressed) - - if err != nil { - t.Fatalf("Error when creating tar: %s", err) - } - - tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) - - if err != nil { - t.Fatalf("Error when executing GetContextFromReader: %s", err) - } - - tarReader := tar.NewReader(tarArchive) - - header, err := tarReader.Next() - - if err != nil { - t.Fatalf("Error when reading tar archive: %s", err) - } - - if header.Name != DefaultDockerfileName { - t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) - } - - buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) - contents := buff.String() - - _, err = tarReader.Next() - - if err != io.EOF { - t.Fatalf("Tar stream too long: %s", err) - } - - if err = tarArchive.Close(); err != nil { - t.Fatalf("Error when closing tar stream: %s", err) - } - - if dockerfileContents != contents { - t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestValidateContextDirectoryEmptyContext(t *testing.T) { - // This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81. - // The test will ultimately end up calling filepath.Abs(""). On Windows, - // golang will error. On Linux, golang will return /. Due to there being - // drive letters on Windows, this is probably the correct behaviour for - // Windows. - if runtime.GOOS == "windows" { - t.Skip("Invalid test on Windows") - } - testValidateContextDirectory(t, prepareEmpty, []string{}) -} - -func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) { - testValidateContextDirectory(t, prepareNoFiles, []string{}) -} - -func TestValidateContextDirectoryWithOneFile(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{}) -} - -func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) -} - -// createTestTempDir creates a temporary directory for testing. -// It returns the created path and a cleanup function which is meant to be used as deferred call. -// When an error occurs, it terminates the test. -func createTestTempDir(t *testing.T, dir, prefix string) (string, func()) { - path, err := ioutil.TempDir(dir, prefix) - - if err != nil { - t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) - } - - return path, func() { - err = os.RemoveAll(path) - - if err != nil { - t.Fatalf("Error when removing directory %s: %s", path, err) - } - } -} - -// createTestTempSubdir creates a temporary directory for testing. -// It returns the created path but doesn't provide a cleanup function, -// so createTestTempSubdir should be used only for creating temporary subdirectories -// whose parent directories are properly cleaned up. -// When an error occurs, it terminates the test. -func createTestTempSubdir(t *testing.T, dir, prefix string) string { - path, err := ioutil.TempDir(dir, prefix) - - if err != nil { - t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) - } - - return path -} - -// createTestTempFile creates a temporary file within dir with specific contents and permissions. -// When an error occurs, it terminates the test -func createTestTempFile(t *testing.T, dir, filename, contents string, perm os.FileMode) string { - filePath := filepath.Join(dir, filename) - err := ioutil.WriteFile(filePath, []byte(contents), perm) - - if err != nil { - t.Fatalf("Error when creating %s file: %s", filename, err) - } - - return filePath -} - -// chdir changes current working directory to dir. -// It returns a function which changes working directory back to the previous one. -// This function is meant to be executed as a deferred call. -// When an error occurs, it terminates the test. -func chdir(t *testing.T, dir string) func() { - workingDirectory, err := os.Getwd() - - if err != nil { - t.Fatalf("Error when retrieving working directory: %s", err) - } - - err = os.Chdir(dir) - - if err != nil { - t.Fatalf("Error when changing directory to %s: %s", dir, err) - } - - return func() { - err = os.Chdir(workingDirectory) - - if err != nil { - t.Fatalf("Error when changing back to working directory (%s): %s", workingDirectory, err) - } - } -} diff --git a/cli/command/image/build/context_unix.go b/cli/command/image/build/context_unix.go deleted file mode 100644 index cb2634f079..0000000000 --- a/cli/command/image/build/context_unix.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !windows - -package build - -import ( - "path/filepath" -) - -func getContextRoot(srcPath string) (string, error) { - return filepath.Join(srcPath, "."), nil -} diff --git a/cli/command/image/build/context_windows.go b/cli/command/image/build/context_windows.go deleted file mode 100644 index c577cfa7be..0000000000 --- a/cli/command/image/build/context_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build windows - -package build - -import ( - "path/filepath" - - "github.com/docker/docker/pkg/longpath" -) - -func getContextRoot(srcPath string) (string, error) { - cr, err := filepath.Abs(srcPath) - if err != nil { - return "", err - } - return longpath.AddPrefix(cr), nil -} diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go deleted file mode 100644 index 0df6fa4f77..0000000000 --- a/cli/command/image/client_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package image - -import ( - "io" - "io/ioutil" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - imageTagFunc func(string, string) error - imageSaveFunc func(images []string) (io.ReadCloser, error) - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) - infoFunc func() (types.Info, error) - imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - imageInspectFunc func(image string) (types.ImageInspect, []byte, error) - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error) -} - -func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error { - if cli.imageTagFunc != nil { - return cli.imageTagFunc(image, ref) - } - return nil -} - -func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadCloser, error) { - if cli.imageSaveFunc != nil { - return cli.imageSaveFunc(images) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImageRemove(_ context.Context, image string, - options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - if cli.imageRemoveFunc != nil { - return cli.imageRemoveFunc(image, options) - } - return []types.ImageDeleteResponseItem{}, nil -} - -func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - if cli.imagePushFunc != nil { - return cli.imagePushFunc(ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) Info(_ context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { - if cli.imagePullFunc != nil { - cli.imagePullFunc(ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) { - if cli.imagesPruneFunc != nil { - return cli.imagesPruneFunc(pruneFilter) - } - return types.ImagesPruneReport{}, nil -} - -func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - if cli.imageLoadFunc != nil { - return cli.imageLoadFunc(input, quiet) - } - return types.ImageLoadResponse{}, nil -} - -func (cli *fakeClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) { - if cli.imageListFunc != nil { - return cli.imageListFunc(options) - } - return []types.ImageSummary{{}}, nil -} - -func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, image string) (types.ImageInspect, []byte, error) { - if cli.imageInspectFunc != nil { - return cli.imageInspectFunc(image) - } - return types.ImageInspect{}, nil, nil -} - -func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string, - options types.ImageImportOptions) (io.ReadCloser, error) { - if cli.imageImportFunc != nil { - return cli.imageImportFunc(source, ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.HistoryResponseItem, error) { - if cli.imageHistoryFunc != nil { - return cli.imageHistoryFunc(img) - } - return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil -} diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go deleted file mode 100644 index c3ca61f85b..0000000000 --- a/cli/command/image/cmd.go +++ /dev/null @@ -1,33 +0,0 @@ -package image - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewImageCommand returns a cobra command for `image` subcommands -func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "image", - Short: "Manage images", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewBuildCommand(dockerCli), - NewHistoryCommand(dockerCli), - NewImportCommand(dockerCli), - NewLoadCommand(dockerCli), - NewPullCommand(dockerCli), - NewPushCommand(dockerCli), - NewSaveCommand(dockerCli), - NewTagCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/image/history.go b/cli/command/image/history.go deleted file mode 100644 index 3e98c228bd..0000000000 --- a/cli/command/image/history.go +++ /dev/null @@ -1,64 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" -) - -type historyOptions struct { - image string - - human bool - quiet bool - noTrunc bool - format string -} - -// NewHistoryCommand creates a new `docker history` command -func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { - var opts historyOptions - - cmd := &cobra.Command{ - Use: "history [OPTIONS] IMAGE", - Short: "Show the history of an image", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - return runHistory(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - - return cmd -} - -func runHistory(dockerCli command.Cli, opts historyOptions) error { - ctx := context.Background() - - history, err := dockerCli.Client().ImageHistory(ctx, opts.image) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - - historyCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human), - Trunc: !opts.noTrunc, - } - return formatter.HistoryWrite(historyCtx, opts.human, history) -} diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go deleted file mode 100644 index ec0a64f207..0000000000 --- a/cli/command/image/history_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "regexp" - "testing" - "time" - - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewHistoryCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires exactly 1 argument(s).", - }, - { - name: "client-error", - args: []string{"image:tag"}, - expectedError: "something went wrong", - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewHistoryCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - outputRegex string - imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) - }{ - { - name: "simple", - args: []string{"image:tag"}, - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil - }, - }, - { - name: "quiet", - args: []string{"--quiet", "image:tag"}, - }, - // TODO: This test is failing since the output does not contain an RFC3339 date - //{ - // name: "non-human", - // args: []string{"--human=false", "image:tag"}, - // outputRegex: "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", // RFC3339 date format match - //}, - { - name: "non-human-header", - args: []string{"--human=false", "image:tag"}, - outputRegex: "CREATED\\sAT", - }, - { - name: "quiet-no-trunc", - args: []string{"--quiet", "--no-trunc", "image:tag"}, - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - if tc.outputRegex == "" { - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } else { - match, _ := regexp.MatchString(tc.outputRegex, actual) - assert.True(t, match) - } - } -} diff --git a/cli/command/image/import.go b/cli/command/image/import.go deleted file mode 100644 index 285998f959..0000000000 --- a/cli/command/image/import.go +++ /dev/null @@ -1,88 +0,0 @@ -package image - -import ( - "io" - "os" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - dockeropts "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/urlutil" - "github.com/spf13/cobra" -) - -type importOptions struct { - source string - reference string - changes dockeropts.ListOpts - message string -} - -// NewImportCommand creates a new `docker import` command -func NewImportCommand(dockerCli command.Cli) *cobra.Command { - var opts importOptions - - cmd := &cobra.Command{ - Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]", - Short: "Import the contents from a tarball to create a filesystem image", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.source = args[0] - if len(args) > 1 { - opts.reference = args[1] - } - return runImport(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - opts.changes = dockeropts.NewListOpts(nil) - flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") - flags.StringVarP(&opts.message, "message", "m", "", "Set commit message for imported image") - - return cmd -} - -func runImport(dockerCli command.Cli, opts importOptions) error { - var ( - in io.Reader - srcName = opts.source - ) - - if opts.source == "-" { - in = dockerCli.In() - } else if !urlutil.IsURL(opts.source) { - srcName = "-" - file, err := os.Open(opts.source) - if err != nil { - return err - } - defer file.Close() - in = file - } - - source := types.ImageImportSource{ - Source: in, - SourceName: srcName, - } - - options := types.ImageImportOptions{ - Message: opts.message, - Changes: opts.changes.GetAll(), - } - - clnt := dockerCli.Client() - - responseBody, err := clnt.ImageImport(context.Background(), source, opts.reference, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go deleted file mode 100644 index c62d610abc..0000000000 --- a/cli/command/image/import_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewImportCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - name: "import-failed", - args: []string{"testdata/import-command-success.input.txt"}, - expectedError: "something went wrong", - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - return nil, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewImportCommandInvalidFile(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) - testutil.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file") -} - -func TestNewImportCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - }{ - { - name: "simple", - args: []string{"testdata/import-command-success.input.txt"}, - }, - { - name: "terminal-source", - args: []string{"-"}, - }, - { - name: "double", - args: []string{"-", "image:local"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "image:local", ref) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - { - name: "message", - args: []string{"--message", "test message", "-"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "test message", options.Message) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - { - name: "change", - args: []string{"--change", "ENV DEBUG true", "-"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "ENV DEBUG true", options.Changes[0]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go deleted file mode 100644 index fe47310cfe..0000000000 --- a/cli/command/image/inspect.go +++ /dev/null @@ -1,44 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - format string - refs []string -} - -// newInspectCommand creates a new cobra.Command for `docker image inspect` -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] IMAGE [IMAGE...]", - Short: "Display detailed information on one or more images", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRefFunc := func(ref string) (interface{}, []byte, error) { - return client.ImageInspectWithRaw(ctx, ref) - } - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) -} diff --git a/cli/command/image/inspect_test.go b/cli/command/image/inspect_test.go deleted file mode 100644 index 7299ad7d86..0000000000 --- a/cli/command/image/inspect_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNewInspectCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewInspectCommandSuccess(t *testing.T) { - imageInspectInvocationCount := 0 - testCases := []struct { - name string - args []string - imageCount int - imageInspectFunc func(image string) (types.ImageInspect, []byte, error) - }{ - { - name: "simple", - args: []string{"image"}, - imageCount: 1, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - assert.Equal(t, "image", image) - return types.ImageInspect{}, nil, nil - }, - }, - { - name: "format", - imageCount: 1, - args: []string{"--format='{{.ID}}'", "image"}, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - return types.ImageInspect{ID: image}, nil, nil - }, - }, - { - name: "simple-many", - args: []string{"image1", "image2"}, - imageCount: 2, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - if imageInspectInvocationCount == 1 { - assert.Equal(t, "image1", image) - } else { - assert.Equal(t, "image2", image) - } - return types.ImageInspect{}, nil, nil - }, - }, - } - for _, tc := range testCases { - imageInspectInvocationCount = 0 - buf := new(bytes.Buffer) - cmd := newInspectCommand(test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - assert.Equal(t, imageInspectInvocationCount, tc.imageCount) - } -} diff --git a/cli/command/image/list.go b/cli/command/image/list.go deleted file mode 100644 index 862183832b..0000000000 --- a/cli/command/image/list.go +++ /dev/null @@ -1,96 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type imagesOptions struct { - matchName string - - quiet bool - all bool - noTrunc bool - showDigests bool - format string - filter opts.FilterOpt -} - -// NewImagesCommand creates a new `docker images` command -func NewImagesCommand(dockerCli command.Cli) *cobra.Command { - opts := imagesOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "images [OPTIONS] [REPOSITORY[:TAG]]", - Short: "List images", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.matchName = args[0] - } - return runImages(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all images (default hides intermediate images)") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.BoolVar(&opts.showDigests, "digests", false, "Show digests") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewImagesCommand(dockerCli) - cmd.Aliases = []string{"images", "list"} - cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" - return &cmd -} - -func runImages(dockerCli command.Cli, opts imagesOptions) error { - ctx := context.Background() - - filters := opts.filter.Value() - if opts.matchName != "" { - filters.Add("reference", opts.matchName) - } - - options := types.ImageListOptions{ - All: opts.all, - Filters: filters, - } - - images, err := dockerCli.Client().ImageList(ctx, options) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ImagesFormat - } else { - format = formatter.TableFormatKey - } - } - - imageCtx := formatter.ImageContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests), - Trunc: !opts.noTrunc, - }, - Digest: opts.showDigests, - } - return formatter.ImageWrite(imageCtx, images) -} diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go deleted file mode 100644 index 39905f8fdd..0000000000 --- a/cli/command/image/list_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewImagesCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - }{ - { - name: "wrong-args", - args: []string{"arg1", "arg2"}, - expectedError: "requires at most 1 argument(s).", - }, - { - name: "failed-list", - expectedError: "something went wrong", - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - return []types.ImageSummary{{}}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewImagesCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageFormat string - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - }{ - { - name: "simple", - }, - { - name: "format", - imageFormat: "raw", - }, - { - name: "quiet-format", - args: []string{"-q"}, - imageFormat: "table", - }, - { - name: "match-name", - args: []string{"image"}, - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - assert.Equal(t, "image", options.Filters.Get("reference")[0]) - return []types.ImageSummary{{}}, nil - }, - }, - { - name: "filters", - args: []string{"--filter", "name=value"}, - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - assert.Equal(t, "value", options.Filters.Get("name")[0]) - return []types.ImageSummary{{}}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, buf) - cli.SetConfigfile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) - cmd := NewImagesCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} - -func TestNewListCommandAlias(t *testing.T) { - cmd := newListCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - assert.True(t, cmd.HasAlias("images")) - assert.True(t, cmd.HasAlias("list")) - assert.False(t, cmd.HasAlias("other")) -} diff --git a/cli/command/image/load.go b/cli/command/image/load.go deleted file mode 100644 index 6bd1f726f7..0000000000 --- a/cli/command/image/load.go +++ /dev/null @@ -1,77 +0,0 @@ -package image - -import ( - "io" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/system" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type loadOptions struct { - input string - quiet bool -} - -// NewLoadCommand creates a new `docker load` command -func NewLoadCommand(dockerCli command.Cli) *cobra.Command { - var opts loadOptions - - cmd := &cobra.Command{ - Use: "load [OPTIONS]", - Short: "Load an image from a tar archive or STDIN", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runLoad(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output") - - return cmd -} - -func runLoad(dockerCli command.Cli, opts loadOptions) error { - - var input io.Reader = dockerCli.In() - if opts.input != "" { - // We use system.OpenSequential to use sequential file access on Windows, avoiding - // depleting the standby list un-necessarily. On Linux, this equates to a regular os.Open. - file, err := system.OpenSequential(opts.input) - if err != nil { - return err - } - defer file.Close() - input = file - } - - // To avoid getting stuck, verify that a tar file is given either in - // the input flag or through stdin and if not display an error message and exit. - if opts.input == "" && dockerCli.In().IsTerminal() { - return errors.Errorf("requested load from stdin, but stdin is empty") - } - - if !dockerCli.Out().IsTerminal() { - opts.quiet = true - } - response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet) - if err != nil { - return err - } - defer response.Body.Close() - - if response.Body != nil && response.JSON { - return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil) - } - - _, err = io.Copy(dockerCli.Out(), response.Body) - return err -} diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go deleted file mode 100644 index d5c6c06841..0000000000 --- a/cli/command/image/load_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewLoadCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - isTerminalIn bool - expectedError string - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - }{ - { - name: "wrong-args", - args: []string{"arg"}, - expectedError: "accepts no argument(s).", - }, - { - name: "input-to-terminal", - isTerminalIn: true, - expectedError: "requested load from stdin, but stdin is empty", - }, - { - name: "pull-error", - expectedError: "something went wrong", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, new(bytes.Buffer)) - cli.In().SetIsTerminal(tc.isTerminalIn) - cmd := NewLoadCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewLoadCommandInvalidInput(t *testing.T) { - expectedError := "open *" - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs([]string{"--input", "*"}) - err := cmd.Execute() - testutil.ErrorContains(t, err, expectedError) -} - -func TestNewLoadCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - }{ - { - name: "simple", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil - }, - }, - { - name: "json", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - json := "{\"ID\": \"1\"}" - return types.ImageLoadResponse{ - Body: ioutil.NopCloser(strings.NewReader(json)), - JSON: true, - }, nil - }, - }, - { - name: "input-file", - args: []string{"--input", "testdata/load-command-success.input.txt"}, - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go deleted file mode 100644 index 59c225be19..0000000000 --- a/cli/command/image/prune.go +++ /dev/null @@ -1,95 +0,0 @@ -package image - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - all bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove unused images", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const ( - allImageWarning = `WARNING! This will remove all images without at least one container associated to them. -Are you sure you want to continue?` - danglingWarning = `WARNING! This will remove all dangling images. -Are you sure you want to continue?` -) - -func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := opts.filter.Value() - pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) - pruneFilters = command.PruneFilters(dockerCli, pruneFilters) - - warning := danglingWarning - if opts.all { - warning = allImageWarning - } - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().ImagesPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.ImagesDeleted) > 0 { - output = "Deleted Images:\n" - for _, st := range report.ImagesDeleted { - if st.Untagged != "" { - output += fmt.Sprintln("untagged:", st.Untagged) - } else { - output += fmt.Sprintln("deleted:", st.Deleted) - } - } - spaceReclaimed = report.SpaceReclaimed - } - - return -} - -// RunPrune calls the Image Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) -} diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go deleted file mode 100644 index 2ac51578c8..0000000000 --- a/cli/command/image/prune_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewPruneCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - }{ - { - name: "wrong-args", - args: []string{"something"}, - expectedError: "accepts no argument(s).", - }, - { - name: "prune-error", - args: []string{"--force"}, - expectedError: "something went wrong", - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - return types.ImagesPruneReport{}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPruneCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - }{ - { - name: "all", - args: []string{"--all"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "false", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{}, nil - }, - }, - { - name: "force-deleted", - args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "true", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{ - ImagesDeleted: []types.ImageDeleteResponseItem{{Deleted: "image1"}}, - SpaceReclaimed: 1, - }, nil - }, - }, - { - name: "force-untagged", - args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "true", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{ - ImagesDeleted: []types.ImageDeleteResponseItem{{Untagged: "image1"}}, - SpaceReclaimed: 2, - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go deleted file mode 100644 index 7561e47b1d..0000000000 --- a/cli/command/image/pull.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pullOptions struct { - remote string - all bool -} - -// NewPullCommand creates a new `docker pull` command -func NewPullCommand(dockerCli command.Cli) *cobra.Command { - var opts pullOptions - - cmd := &cobra.Command{ - Use: "pull [OPTIONS] NAME[:TAG|@DIGEST]", - Short: "Pull an image or a repository from a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.remote = args[0] - return runPull(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") - command.AddTrustVerificationFlags(flags) - - return cmd -} - -func runPull(dockerCli command.Cli, opts pullOptions) error { - distributionRef, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return err - } - if opts.all && !reference.IsNameOnly(distributionRef) { - return errors.New("tag can't be used with --all-tags/-a") - } - - if !opts.all && reference.IsNameOnly(distributionRef) { - distributionRef = reference.TagNameOnly(distributionRef) - if tagged, ok := distributionRef.(reference.Tagged); ok { - fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag()) - } - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(distributionRef) - if err != nil { - return err - } - - ctx := context.Background() - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull") - - // Check if reference has a digest - _, isCanonical := distributionRef.(reference.Canonical) - if command.IsTrusted() && !isCanonical { - err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege) - } else { - err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all) - } - if err != nil { - if strings.Contains(err.Error(), "when fetching 'plugin'") { - return errors.New(err.Error() + " - Use `docker plugin install`") - } - return err - } - - return nil -} diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go deleted file mode 100644 index b4055dc397..0000000000 --- a/cli/command/image/pull_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/docker/docker/registry" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestNewPullCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, - authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "wrong-args", - expectedError: "requires exactly 1 argument(s).", - args: []string{}, - }, - { - name: "invalid-name", - expectedError: "invalid reference format: repository name must be lowercase", - args: []string{"UPPERCASE_REPO"}, - }, - { - name: "all-tags-with-tag", - expectedError: "tag can't be used with --all-tags/-a", - args: []string{"--all-tags", "image:tag"}, - }, - { - name: "pull-error", - args: []string{"--disable-content-trust=false", "image:tag"}, - expectedError: "you are not authorized to perform this operation: server returned 401.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPullCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, - authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "simple", - args: []string{"image:tag"}, - }, - { - name: "simple-no-tag", - args: []string{"image"}, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/push.go b/cli/command/image/push.go deleted file mode 100644 index 4f773deb80..0000000000 --- a/cli/command/image/push.go +++ /dev/null @@ -1,61 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -// NewPushCommand creates a new `docker push` command -func NewPushCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "push [OPTIONS] NAME[:TAG]", - Short: "Push an image or a repository to a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPush(dockerCli, args[0]) - }, - } - - flags := cmd.Flags() - - command.AddTrustSigningFlags(flags) - - return cmd -} - -func runPush(dockerCli command.Cli, remote string) error { - ref, err := reference.ParseNormalizedNamed(remote) - if err != nil { - return err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - ctx := context.Background() - - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") - - if command.IsTrusted() { - return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) - } - - responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege) - if err != nil { - return err - } - - defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go deleted file mode 100644 index 9ecb9db680..0000000000 --- a/cli/command/image/push_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestNewPushCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires exactly 1 argument(s).", - }, - { - name: "invalid-name", - args: []string{"UPPERCASE_REPO"}, - expectedError: "invalid reference format: repository name must be lowercase", - }, - { - name: "push-failed", - args: []string{"image:repo"}, - expectedError: "Failed to push", - imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push") - }, - }, - { - name: "trust-error", - args: []string{"--disable-content-trust=false", "image:repo"}, - expectedError: "you are not authorized to perform this operation: server returned 401.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPushCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - trustedPushFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, - ref reference.Named, authConfig types.AuthConfig, - requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "simple", - args: []string{"image:tag"}, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{ - imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go deleted file mode 100644 index 0680ed05f4..0000000000 --- a/cli/command/image/remove.go +++ /dev/null @@ -1,78 +0,0 @@ -package image - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type removeOptions struct { - force bool - noPrune bool -} - -// NewRemoveCommand creates a new `docker remove` command -func NewRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rmi [OPTIONS] IMAGE [IMAGE...]", - Short: "Remove one or more images", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, opts, args) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.force, "force", "f", false, "Force removal of the image") - flags.BoolVar(&opts.noPrune, "no-prune", false, "Do not delete untagged parents") - - return cmd -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRemoveCommand(dockerCli) - cmd.Aliases = []string{"rmi", "remove"} - cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" - return &cmd -} - -func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error { - client := dockerCli.Client() - ctx := context.Background() - - options := types.ImageRemoveOptions{ - Force: opts.force, - PruneChildren: !opts.noPrune, - } - - var errs []string - for _, image := range images { - dels, err := client.ImageRemove(ctx, image, options) - if err != nil { - errs = append(errs, err.Error()) - } else { - for _, del := range dels { - if del.Deleted != "" { - fmt.Fprintf(dockerCli.Out(), "Deleted: %s\n", del.Deleted) - } else { - fmt.Fprintf(dockerCli.Out(), "Untagged: %s\n", del.Untagged) - } - } - } - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go deleted file mode 100644 index a1b3c4b9b6..0000000000 --- a/cli/command/image/remove_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewRemoveCommandAlias(t *testing.T) { - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - assert.True(t, cmd.HasAlias("rmi")) - assert.True(t, cmd.HasAlias("remove")) - assert.False(t, cmd.HasAlias("other")) -} - -func TestNewRemoveCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - }{ - { - name: "wrong args", - expectedError: "requires at least 1 argument(s).", - }, - { - name: "ImageRemove fail", - args: []string{"arg1"}, - expectedError: "error removing image", - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.False(t, options.Force) - assert.True(t, options.PruneChildren) - return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image") - }, - }, - } - for _, tc := range testCases { - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ - imageRemoveFunc: tc.imageRemoveFunc, - }, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewRemoveCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - }{ - { - name: "Image Deleted", - args: []string{"image1"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.Equal(t, "image1", image) - return []types.ImageDeleteResponseItem{{Deleted: image}}, nil - }, - }, - { - name: "Image Untagged", - args: []string{"image1"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.Equal(t, "image1", image) - return []types.ImageDeleteResponseItem{{Untagged: image}}, nil - }, - }, - { - name: "Image Deleted and Untagged", - args: []string{"image1", "image2"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - if image == "image1" { - return []types.ImageDeleteResponseItem{{Untagged: image}}, nil - } - return []types.ImageDeleteResponseItem{{Deleted: image}}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ - imageRemoveFunc: tc.imageRemoveFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/save.go b/cli/command/image/save.go deleted file mode 100644 index d67ccb7d2c..0000000000 --- a/cli/command/image/save.go +++ /dev/null @@ -1,56 +0,0 @@ -package image - -import ( - "io" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type saveOptions struct { - images []string - output string -} - -// NewSaveCommand creates a new `docker save` command -func NewSaveCommand(dockerCli command.Cli) *cobra.Command { - var opts saveOptions - - cmd := &cobra.Command{ - Use: "save [OPTIONS] IMAGE [IMAGE...]", - Short: "Save one or more images to a tar archive (streamed to STDOUT by default)", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.images = args - return runSave(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - - return cmd -} - -func runSave(dockerCli command.Cli, opts saveOptions) error { - if opts.output == "" && dockerCli.Out().IsTerminal() { - return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") - } - - responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images) - if err != nil { - return err - } - defer responseBody.Close() - - if opts.output == "" { - _, err := io.Copy(dockerCli.Out(), responseBody) - return err - } - - return command.CopyToFile(opts.output, responseBody) -} diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go deleted file mode 100644 index 75475a2b3b..0000000000 --- a/cli/command/image/save_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewSaveCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - isTerminal bool - expectedError string - imageSaveFunc func(images []string) (io.ReadCloser, error) - }{ - { - name: "wrong args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - name: "output to terminal", - args: []string{"output", "file", "arg1"}, - isTerminal: true, - expectedError: "Cowardly refusing to save to a terminal. Use the -o flag or redirect.", - }, - { - name: "ImageSave fail", - args: []string{"arg1"}, - isTerminal: false, - expectedError: "error saving image", - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image") - }, - }, - } - for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}, new(bytes.Buffer)) - cli.Out().SetIsTerminal(tc.isTerminal) - cmd := NewSaveCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewSaveCommandSuccess(t *testing.T) { - testCases := []struct { - args []string - isTerminal bool - imageSaveFunc func(images []string) (io.ReadCloser, error) - deferredFunc func() - }{ - { - args: []string{"-o", "save_tmp_file", "arg1"}, - isTerminal: true, - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - require.Len(t, images, 1) - assert.Equal(t, "arg1", images[0]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - deferredFunc: func() { - os.Remove("save_tmp_file") - }, - }, - { - args: []string{"arg1", "arg2"}, - isTerminal: false, - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - require.Len(t, images, 2) - assert.Equal(t, "arg1", images[0]) - assert.Equal(t, "arg2", images[1]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - } - for _, tc := range testCases { - cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - if tc.deferredFunc != nil { - tc.deferredFunc() - } - } -} diff --git a/cli/command/image/tag.go b/cli/command/image/tag.go deleted file mode 100644 index 409d83afee..0000000000 --- a/cli/command/image/tag.go +++ /dev/null @@ -1,41 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type tagOptions struct { - image string - name string -} - -// NewTagCommand creates a new `docker tag` command -func NewTagCommand(dockerCli command.Cli) *cobra.Command { - var opts tagOptions - - cmd := &cobra.Command{ - Use: "tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", - Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - opts.name = args[1] - return runTag(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - return cmd -} - -func runTag(dockerCli command.Cli, opts tagOptions) error { - ctx := context.Background() - - return dockerCli.Client().ImageTag(ctx, opts.image, opts.name) -} diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go deleted file mode 100644 index b37bf30753..0000000000 --- a/cli/command/image/tag_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package image - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestCliNewTagCommandErrors(t *testing.T) { - testCases := [][]string{ - {}, - {"image1"}, - {"image1", "image2", "image3"}, - } - expectedError := "\"tag\" requires exactly 2 argument(s)." - buf := new(bytes.Buffer) - for _, args := range testCases { - cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetArgs(args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), expectedError) - } -} - -func TestCliNewTagCommand(t *testing.T) { - buf := new(bytes.Buffer) - cmd := NewTagCommand( - test.NewFakeCli(&fakeClient{ - imageTagFunc: func(image string, ref string) error { - assert.Equal(t, "image1", image) - assert.Equal(t, "image2", ref) - return nil - }, - }, buf)) - cmd.SetArgs([]string{"image1", "image2"}) - cmd.SetOutput(ioutil.Discard) - assert.NoError(t, cmd.Execute()) - value, _ := cmd.Flags().GetBool("interspersed") - assert.False(t, value) -} diff --git a/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden b/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden deleted file mode 100644 index 65103f6354..0000000000 --- a/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden +++ /dev/null @@ -1 +0,0 @@ -1234567890123456789 diff --git a/cli/command/image/testdata/history-command-success.quiet.golden b/cli/command/image/testdata/history-command-success.quiet.golden deleted file mode 100644 index 42c7c82cc8..0000000000 --- a/cli/command/image/testdata/history-command-success.quiet.golden +++ /dev/null @@ -1 +0,0 @@ -tag diff --git a/cli/command/image/testdata/history-command-success.simple.golden b/cli/command/image/testdata/history-command-success.simple.golden deleted file mode 100644 index 8aa590526f..0000000000 --- a/cli/command/image/testdata/history-command-success.simple.golden +++ /dev/null @@ -1,2 +0,0 @@ -IMAGE CREATED CREATED BY SIZE COMMENT -123456789012 Less than a second ago 0B diff --git a/cli/command/image/testdata/import-command-success.input.txt b/cli/command/image/testdata/import-command-success.input.txt deleted file mode 100644 index 7ab5949b13..0000000000 --- a/cli/command/image/testdata/import-command-success.input.txt +++ /dev/null @@ -1 +0,0 @@ -file input test \ No newline at end of file diff --git a/cli/command/image/testdata/inspect-command-success.format.golden b/cli/command/image/testdata/inspect-command-success.format.golden deleted file mode 100644 index f934996b07..0000000000 --- a/cli/command/image/testdata/inspect-command-success.format.golden +++ /dev/null @@ -1 +0,0 @@ -'image' diff --git a/cli/command/image/testdata/inspect-command-success.simple-many.golden b/cli/command/image/testdata/inspect-command-success.simple-many.golden deleted file mode 100644 index d4042589f8..0000000000 --- a/cli/command/image/testdata/inspect-command-success.simple-many.golden +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - }, - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - } -] diff --git a/cli/command/image/testdata/inspect-command-success.simple.golden b/cli/command/image/testdata/inspect-command-success.simple.golden deleted file mode 100644 index 802c52469b..0000000000 --- a/cli/command/image/testdata/inspect-command-success.simple.golden +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - } -] diff --git a/cli/command/image/testdata/list-command-success.filters.golden b/cli/command/image/testdata/list-command-success.filters.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.filters.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.format.golden b/cli/command/image/testdata/list-command-success.format.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/list-command-success.match-name.golden b/cli/command/image/testdata/list-command-success.match-name.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.match-name.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.quiet-format.golden b/cli/command/image/testdata/list-command-success.quiet-format.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/list-command-success.simple.golden b/cli/command/image/testdata/list-command-success.simple.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.simple.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/load-command-success.input-file.golden b/cli/command/image/testdata/load-command-success.input-file.golden deleted file mode 100644 index 51da4200ab..0000000000 --- a/cli/command/image/testdata/load-command-success.input-file.golden +++ /dev/null @@ -1 +0,0 @@ -Success \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.input.txt b/cli/command/image/testdata/load-command-success.input.txt deleted file mode 100644 index 7ab5949b13..0000000000 --- a/cli/command/image/testdata/load-command-success.input.txt +++ /dev/null @@ -1 +0,0 @@ -file input test \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.json.golden b/cli/command/image/testdata/load-command-success.json.golden deleted file mode 100644 index c17f16ecd7..0000000000 --- a/cli/command/image/testdata/load-command-success.json.golden +++ /dev/null @@ -1 +0,0 @@ -1: diff --git a/cli/command/image/testdata/load-command-success.simple.golden b/cli/command/image/testdata/load-command-success.simple.golden deleted file mode 100644 index 51da4200ab..0000000000 --- a/cli/command/image/testdata/load-command-success.simple.golden +++ /dev/null @@ -1 +0,0 @@ -Success \ No newline at end of file diff --git a/cli/command/image/testdata/prune-command-success.all.golden b/cli/command/image/testdata/prune-command-success.all.golden deleted file mode 100644 index 4d1445280c..0000000000 --- a/cli/command/image/testdata/prune-command-success.all.golden +++ /dev/null @@ -1,2 +0,0 @@ -WARNING! This will remove all images without at least one container associated to them. -Are you sure you want to continue? [y/N] Total reclaimed space: 0B diff --git a/cli/command/image/testdata/prune-command-success.force-deleted.golden b/cli/command/image/testdata/prune-command-success.force-deleted.golden deleted file mode 100644 index 1b6efd4a99..0000000000 --- a/cli/command/image/testdata/prune-command-success.force-deleted.golden +++ /dev/null @@ -1,4 +0,0 @@ -Deleted Images: -deleted: image1 - -Total reclaimed space: 1B diff --git a/cli/command/image/testdata/prune-command-success.force-untagged.golden b/cli/command/image/testdata/prune-command-success.force-untagged.golden deleted file mode 100644 index 725468fe56..0000000000 --- a/cli/command/image/testdata/prune-command-success.force-untagged.golden +++ /dev/null @@ -1,4 +0,0 @@ -Deleted Images: -untagged: image1 - -Total reclaimed space: 2B diff --git a/cli/command/image/testdata/pull-command-success.simple-no-tag.golden b/cli/command/image/testdata/pull-command-success.simple-no-tag.golden deleted file mode 100644 index 946de409a4..0000000000 --- a/cli/command/image/testdata/pull-command-success.simple-no-tag.golden +++ /dev/null @@ -1 +0,0 @@ -Using default tag: latest diff --git a/cli/command/image/testdata/pull-command-success.simple.golden b/cli/command/image/testdata/pull-command-success.simple.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden deleted file mode 100644 index 4efc53719d..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden +++ /dev/null @@ -1,4 +0,0 @@ -Untagged: image1 -Deleted: image2 -Untagged: image1 -Deleted: image2 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted.golden b/cli/command/image/testdata/remove-command-success.Image Deleted.golden deleted file mode 100644 index 382724d39f..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Deleted.golden +++ /dev/null @@ -1,2 +0,0 @@ -Deleted: image1 -Deleted: image1 diff --git a/cli/command/image/testdata/remove-command-success.Image Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Untagged.golden deleted file mode 100644 index c795dac19f..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Untagged.golden +++ /dev/null @@ -1,2 +0,0 @@ -Untagged: image1 -Untagged: image1 diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go deleted file mode 100644 index 63fb6f09fb..0000000000 --- a/cli/command/image/trust.go +++ /dev/null @@ -1,382 +0,0 @@ -package image - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "io" - "path" - "sort" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/docker/notary/client" - "github.com/docker/notary/tuf/data" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type target struct { - name string - digest digest.Digest - size int64 -} - -// trustedPush handles content trust pushing of an image -func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) - if err != nil { - return err - } - - defer responseBody.Close() - - return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody) -} - -// PushTrustedReference pushes a canonical reference to the trust server. -func PushTrustedReference(cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error { - // If it is a trusted push we would like to find the target entry which match the - // tag provided in the function and then do an AddTarget later. - target := &client.Target{} - // Count the times of calling for handleTarget, - // if it is called more that once, that should be considered an error in a trusted push. - cnt := 0 - handleTarget := func(aux *json.RawMessage) { - cnt++ - if cnt > 1 { - // handleTarget should only be called one. This will be treated as an error. - return - } - - var pushResult types.PushResult - err := json.Unmarshal(*aux, &pushResult) - if err == nil && pushResult.Tag != "" { - if dgst, err := digest.Parse(pushResult.Digest); err == nil { - h, err := hex.DecodeString(dgst.Hex()) - if err != nil { - target = nil - return - } - target.Name = pushResult.Tag - target.Hashes = data.Hashes{string(dgst.Algorithm()): h} - target.Length = int64(pushResult.Size) - } - } - } - - var tag string - switch x := ref.(type) { - case reference.Canonical: - return errors.New("cannot push a digest reference") - case reference.NamedTagged: - tag = x.Tag() - default: - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), nil); err != nil { - return err - } - fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") - return nil - } - - if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), handleTarget); err != nil { - return err - } - - if cnt > 1 { - return errors.Errorf("internal error: only one call to handleTarget expected") - } - - if target == nil { - fmt.Fprintln(cli.Out(), "No targets found, please provide a specific tag in order to sign it") - return nil - } - - fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") - - repo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) - return err - } - - // get the latest repository metadata so we can figure out which roles to sign - err = repo.Update(false) - - switch err.(type) { - case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: - keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) - var rootKeyID string - // always select the first root key - if len(keys) > 0 { - sort.Strings(keys) - rootKeyID = keys[0] - } else { - rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) - if err != nil { - return err - } - rootKeyID = rootPublicKey.ID() - } - - // Initialize the notary repository with a remotely managed snapshot key - if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return trust.NotaryError(repoInfo.Name.Name(), err) - } - fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.Name.Name()) - err = repo.AddTarget(target, data.CanonicalTargetsRole) - case nil: - // already initialized and we have successfully downloaded the latest metadata - err = addTargetToAllSignableRoles(repo, target) - default: - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - if err == nil { - err = repo.Publish() - } - - if err != nil { - fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.Name.Name(), tag, err.Error()) - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.Name.Name(), tag) - return nil -} - -// Attempt to add the image target to all the top level delegation roles we can -// (based on whether we have the signing key and whether the role's path allows -// us to). -// If there are no delegation roles, we add to the targets role. -func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { - var signableRoles []string - - // translate the full key names, which includes the GUN, into just the key IDs - allCanonicalKeyIDs := make(map[string]struct{}) - for fullKeyID := range repo.CryptoService.ListAllKeys() { - allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} - } - - allDelegationRoles, err := repo.GetDelegationRoles() - if err != nil { - return err - } - - // if there are no delegation roles, then just try to sign it into the targets role - if len(allDelegationRoles) == 0 { - return repo.AddTarget(target, data.CanonicalTargetsRole) - } - - // there are delegation roles, find every delegation role we have a key for, and - // attempt to sign into into all those roles. - for _, delegationRole := range allDelegationRoles { - // We do not support signing any delegation role that isn't a direct child of the targets role. - // Also don't bother checking the keys if we can't add the target - // to this role due to path restrictions - if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { - continue - } - - for _, canonicalKeyID := range delegationRole.KeyIDs { - if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { - signableRoles = append(signableRoles, delegationRole.Name) - break - } - } - } - - if len(signableRoles) == 0 { - return errors.Errorf("no valid signing keys for delegation roles") - } - - return repo.AddTarget(target, signableRoles...) -} - -// imagePushPrivileged push the image -func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return nil, err - } - options := types.ImagePushOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - } - - return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) -} - -// trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - var refs []target - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) - return err - } - - if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { - // List all targets - targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return trust.NotaryError(ref.Name(), err) - } - for _, tgt := range targets { - t, err := convertTarget(tgt.Target) - if err != nil { - fmt.Fprintf(cli.Out(), "Skipping target for %q\n", reference.FamiliarName(ref)) - continue - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole { - continue - } - refs = append(refs, t) - } - if len(refs) == 0 { - return trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name())) - } - } else { - t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return trust.NotaryError(ref.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag())) - } - - logrus.Debugf("retrieving target for %s role\n", t.Role) - r, err := convertTarget(t.Target) - if err != nil { - return err - - } - refs = append(refs, r) - } - - for i, r := range refs { - displayTag := r.name - if displayTag != "" { - displayTag = ":" + displayTag - } - fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest) - - trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest) - if err != nil { - return err - } - if err := imagePullPrivileged(ctx, cli, authConfig, reference.FamiliarString(trustedRef), requestPrivilege, false); err != nil { - return err - } - - tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name) - if err != nil { - return err - } - - if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { - return err - } - } - return nil -} - -// imagePullPrivileged pulls the image and displays it to the output -func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - options := types.ImagePullOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - All: all, - } - - responseBody, err := cli.Client().ImagePull(ctx, ref, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) -} - -// TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { - var ( - repoInfo *registry.RepositoryInfo - err error - ) - if rs != nil { - repoInfo, err = rs.ResolveRepository(ref) - } else { - repoInfo, err = registry.ParseRepositoryInfo(ref) - } - if err != nil { - return nil, err - } - - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) - return nil, err - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(repoInfo.Name.Name(), err) - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", ref.Tag())) - } - r, err := convertTarget(t.Target) - if err != nil { - return nil, err - - } - - return reference.WithDigest(reference.TrimNamed(ref), r.digest) -} - -func convertTarget(t client.Target) (target, error) { - h, ok := t.Hashes["sha256"] - if !ok { - return target{}, errors.New("no valid hash, expecting sha256") - } - return target{ - name: t.Name, - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, - }, nil -} - -// TagTrusted tags a trusted ref -func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error { - // Use familiar references when interacting with client and output - familiarRef := reference.FamiliarString(ref) - trustedFamiliarRef := reference.FamiliarString(trustedRef) - - fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) - - return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef) -} diff --git a/cli/command/image/trust_test.go b/cli/command/image/trust_test.go deleted file mode 100644 index 78146465e6..0000000000 --- a/cli/command/image/trust_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package image - -import ( - "os" - "testing" - - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/registry" -) - -func unsetENV() { - os.Unsetenv("DOCKER_CONTENT_TRUST") - os.Unsetenv("DOCKER_CONTENT_TRUST_SERVER") -} - -func TestENVTrustServer(t *testing.T) { - defer unsetENV() - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil { - t.Fatal("Failed to set ENV variable") - } - output, err := trust.Server(indexInfo) - expectedStr := "https://notary-test.com:5000" - if err != nil || output != expectedStr { - t.Fatalf("Expected server to be %s, got %s", expectedStr, output) - } -} - -func TestHTTPENVTrustServer(t *testing.T) { - defer unsetENV() - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil { - t.Fatal("Failed to set ENV variable") - } - _, err := trust.Server(indexInfo) - if err == nil { - t.Fatal("Expected error with invalid scheme") - } -} - -func TestOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} - output, err := trust.Server(indexInfo) - if err != nil || output != registry.NotaryServer { - t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output) - } -} - -func TestNonOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} - output, err := trust.Server(indexInfo) - expectedStr := "https://" + indexInfo.Name - if err != nil || output != expectedStr { - t.Fatalf("Expected server to be %s, got %s", expectedStr, output) - } -} diff --git a/cli/command/in.go b/cli/command/in.go deleted file mode 100644 index 54855c6dc2..0000000000 --- a/cli/command/in.go +++ /dev/null @@ -1,56 +0,0 @@ -package command - -import ( - "errors" - "io" - "os" - "runtime" - - "github.com/docker/docker/pkg/term" -) - -// InStream is an input stream used by the DockerCli to read user input -type InStream struct { - CommonStream - in io.ReadCloser -} - -func (i *InStream) Read(p []byte) (int, error) { - return i.in.Read(p) -} - -// Close implements the Closer interface -func (i *InStream) Close() error { - return i.in.Close() -} - -// SetRawTerminal sets raw mode on the input terminal -func (i *InStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal { - return nil - } - i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.fd) - return err -} - -// CheckTty checks if we are trying to attach to a container tty -// from a non-tty client input stream, and if so, returns an error. -func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { - // In order to attach to a container tty, input stream for the client must - // be a tty itself: redirecting or piping the client standard input is - // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. - if ttyMode && attachStdin && !i.isTerminal { - eText := "the input device is not a TTY" - if runtime.GOOS == "windows" { - return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") - } - return errors.New(eText) - } - return nil -} - -// NewInStream returns a new InStream object from a ReadCloser -func NewInStream(in io.ReadCloser) *InStream { - fd, isTerminal := term.GetFdInfo(in) - return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in} -} diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go deleted file mode 100644 index 13e584ab49..0000000000 --- a/cli/command/inspect/inspector.go +++ /dev/null @@ -1,198 +0,0 @@ -package inspect - -import ( - "bytes" - "encoding/json" - "io" - "strings" - "text/template" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/cli" - "github.com/docker/docker/pkg/templates" - "github.com/pkg/errors" -) - -// Inspector defines an interface to implement to process elements -type Inspector interface { - Inspect(typedElement interface{}, rawElement []byte) error - Flush() error -} - -// TemplateInspector uses a text template to inspect elements. -type TemplateInspector struct { - outputStream io.Writer - buffer *bytes.Buffer - tmpl *template.Template -} - -// NewTemplateInspector creates a new inspector with a template. -func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector { - return &TemplateInspector{ - outputStream: outputStream, - buffer: new(bytes.Buffer), - tmpl: tmpl, - } -} - -// NewTemplateInspectorFromString creates a new TemplateInspector from a string -// which is compiled into a template. -func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) { - if tmplStr == "" { - return NewIndentedInspector(out), nil - } - - tmpl, err := templates.Parse(tmplStr) - if err != nil { - return nil, errors.Errorf("Template parsing error: %s", err) - } - return NewTemplateInspector(out, tmpl), nil -} - -// GetRefFunc is a function which used by Inspect to fetch an object from a -// reference -type GetRefFunc func(ref string) (interface{}, []byte, error) - -// Inspect fetches objects by reference using GetRefFunc and writes the json -// representation to the output writer. -func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error { - inspector, err := NewTemplateInspectorFromString(out, tmplStr) - if err != nil { - return cli.StatusError{StatusCode: 64, Status: err.Error()} - } - - var inspectErrs []string - for _, ref := range references { - element, raw, err := getRef(ref) - if err != nil { - inspectErrs = append(inspectErrs, err.Error()) - continue - } - - if err := inspector.Inspect(element, raw); err != nil { - inspectErrs = append(inspectErrs, err.Error()) - } - } - - if err := inspector.Flush(); err != nil { - logrus.Errorf("%s\n", err) - } - - if len(inspectErrs) != 0 { - return cli.StatusError{ - StatusCode: 1, - Status: strings.Join(inspectErrs, "\n"), - } - } - return nil -} - -// Inspect executes the inspect template. -// It decodes the raw element into a map if the initial execution fails. -// This allows docker cli to parse inspect structs injected with Swarm fields. -func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error { - buffer := new(bytes.Buffer) - if err := i.tmpl.Execute(buffer, typedElement); err != nil { - if rawElement == nil { - return errors.Errorf("Template parsing error: %v", err) - } - return i.tryRawInspectFallback(rawElement) - } - i.buffer.Write(buffer.Bytes()) - i.buffer.WriteByte('\n') - return nil -} - -// tryRawInspectFallback executes the inspect template with a raw interface. -// This allows docker cli to parse inspect structs injected with Swarm fields. -func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { - var raw interface{} - buffer := new(bytes.Buffer) - rdr := bytes.NewReader(rawElement) - dec := json.NewDecoder(rdr) - - if rawErr := dec.Decode(&raw); rawErr != nil { - return errors.Errorf("unable to read inspect data: %v", rawErr) - } - - tmplMissingKey := i.tmpl.Option("missingkey=error") - if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { - return errors.Errorf("Template parsing error: %v", rawErr) - } - - i.buffer.Write(buffer.Bytes()) - i.buffer.WriteByte('\n') - return nil -} - -// Flush writes the result of inspecting all elements into the output stream. -func (i *TemplateInspector) Flush() error { - if i.buffer.Len() == 0 { - _, err := io.WriteString(i.outputStream, "\n") - return err - } - _, err := io.Copy(i.outputStream, i.buffer) - return err -} - -// IndentedInspector uses a buffer to stop the indented representation of an element. -type IndentedInspector struct { - outputStream io.Writer - elements []interface{} - rawElements [][]byte -} - -// NewIndentedInspector generates a new IndentedInspector. -func NewIndentedInspector(outputStream io.Writer) Inspector { - return &IndentedInspector{ - outputStream: outputStream, - } -} - -// Inspect writes the raw element with an indented json format. -func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) error { - if rawElement != nil { - i.rawElements = append(i.rawElements, rawElement) - } else { - i.elements = append(i.elements, typedElement) - } - return nil -} - -// Flush writes the result of inspecting all elements into the output stream. -func (i *IndentedInspector) Flush() error { - if len(i.elements) == 0 && len(i.rawElements) == 0 { - _, err := io.WriteString(i.outputStream, "[]\n") - return err - } - - var buffer io.Reader - if len(i.rawElements) > 0 { - bytesBuffer := new(bytes.Buffer) - bytesBuffer.WriteString("[") - for idx, r := range i.rawElements { - bytesBuffer.Write(r) - if idx < len(i.rawElements)-1 { - bytesBuffer.WriteString(",") - } - } - bytesBuffer.WriteString("]") - indented := new(bytes.Buffer) - if err := json.Indent(indented, bytesBuffer.Bytes(), "", " "); err != nil { - return err - } - buffer = indented - } else { - b, err := json.MarshalIndent(i.elements, "", " ") - if err != nil { - return err - } - buffer = bytes.NewReader(b) - } - - if _, err := io.Copy(i.outputStream, buffer); err != nil { - return err - } - _, err := io.WriteString(i.outputStream, "\n") - return err -} diff --git a/cli/command/inspect/inspector_test.go b/cli/command/inspect/inspector_test.go deleted file mode 100644 index 9085230ac5..0000000000 --- a/cli/command/inspect/inspector_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package inspect - -import ( - "bytes" - "strings" - "testing" - - "github.com/docker/docker/pkg/templates" -) - -type testElement struct { - DNS string `json:"Dns"` -} - -func TestTemplateInspectorDefault(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n" { - t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorEmpty(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "\n" { - t.Fatalf("Expected `\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorTemplateError(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Foo}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - err = i.Inspect(testElement{"0.0.0.0"}, nil) - if err == nil { - t.Fatal("Expected error got nil") - } - - if !strings.HasPrefix(err.Error(), "Template parsing error") { - t.Fatalf("Expected template error, got %v", err) - } -} - -func TestTemplateInspectorRawFallback(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Dns}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n" { - t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorRawFallbackError(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Dns}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - err = i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Foo": "0.0.0.0"}`)) - if err == nil { - t.Fatal("Expected error got nil") - } - - if !strings.HasPrefix(err.Error(), "Template parsing error") { - t.Fatalf("Expected template error, got %v", err) - } -} - -func TestTemplateInspectorMultiple(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n1.1.1.1\n" { - t.Fatalf("Expected `0.0.0.0\\n1.1.1.1\\n`, got `%s`", b.String()) - } -} - -func TestIndentedInspectorDefault(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorMultiple(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0" - }, - { - "Dns": "1.1.1.1" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorEmpty(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := "[]\n" - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorRawElements(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0", "Node": "0"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Inspect(testElement{"1.1.1.1"}, []byte(`{"Dns": "1.1.1.1", "Node": "1"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0", - "Node": "0" - }, - { - "Dns": "1.1.1.1", - "Node": "1" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go deleted file mode 100644 index ab8393cded..0000000000 --- a/cli/command/network/cmd.go +++ /dev/null @@ -1,28 +0,0 @@ -package network - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewNetworkCommand returns a cobra command for `network` subcommands -func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "network", - Short: "Manage networks", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - newConnectCommand(dockerCli), - newCreateCommand(dockerCli), - newDisconnectCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/network/connect.go b/cli/command/network/connect.go deleted file mode 100644 index bc90ddaba7..0000000000 --- a/cli/command/network/connect.go +++ /dev/null @@ -1,63 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type connectOptions struct { - network string - container string - ipaddress string - ipv6address string - links opts.ListOpts - aliases []string - linklocalips []string -} - -func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := connectOptions{ - links: opts.NewListOpts(opts.ValidateLink), - } - - cmd := &cobra.Command{ - Use: "connect [OPTIONS] NETWORK CONTAINER", - Short: "Connect a container to a network", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.network = args[0] - opts.container = args[1] - return runConnect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.ipaddress, "ip", "", "IPv4 address (e.g., 172.30.100.104)") - flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") - flags.Var(&opts.links, "link", "Add link to another container") - flags.StringSliceVar(&opts.aliases, "alias", []string{}, "Add network-scoped alias for the container") - flags.StringSliceVar(&opts.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") - - return cmd -} - -func runConnect(dockerCli *command.DockerCli, opts connectOptions) error { - client := dockerCli.Client() - - epConfig := &network.EndpointSettings{ - IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: opts.ipaddress, - IPv6Address: opts.ipv6address, - LinkLocalIPs: opts.linklocalips, - }, - Links: opts.links.GetAll(), - Aliases: opts.aliases, - } - - return client.NetworkConnect(context.Background(), opts.network, opts.container, epConfig) -} diff --git a/cli/command/network/create.go b/cli/command/network/create.go deleted file mode 100644 index 90119af919..0000000000 --- a/cli/command/network/create.go +++ /dev/null @@ -1,232 +0,0 @@ -package network - -import ( - "fmt" - "net" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type createOptions struct { - name string - driver string - driverOpts opts.MapOpts - labels opts.ListOpts - internal bool - ipv6 bool - attachable bool - ingress bool - - ipamDriver string - ipamSubnet []string - ipamIPRange []string - ipamGateway []string - ipamAux opts.MapOpts - ipamOpt opts.MapOpts -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := createOptions{ - driverOpts: *opts.NewMapOpts(nil, nil), - labels: opts.NewListOpts(opts.ValidateEnv), - ipamAux: *opts.NewMapOpts(nil, nil), - ipamOpt: *opts.NewMapOpts(nil, nil), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] NETWORK", - Short: "Create a network", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runCreate(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.driver, "driver", "d", "bridge", "Driver to manage the Network") - flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.Var(&opts.labels, "label", "Set metadata on a network") - flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network") - flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") - flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") - flags.SetAnnotation("attachable", "version", []string{"1.25"}) - flags.BoolVar(&opts.ingress, "ingress", false, "Create swarm routing-mesh network") - flags.SetAnnotation("ingress", "version", []string{"1.29"}) - - flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver") - flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") - flags.StringSliceVar(&opts.ipamIPRange, "ip-range", []string{}, "Allocate container ip from a sub-range") - flags.StringSliceVar(&opts.ipamGateway, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") - - flags.Var(&opts.ipamAux, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") - flags.Var(&opts.ipamOpt, "ipam-opt", "Set IPAM driver specific options") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { - client := dockerCli.Client() - - ipamCfg, err := consolidateIpam(opts.ipamSubnet, opts.ipamIPRange, opts.ipamGateway, opts.ipamAux.GetAll()) - if err != nil { - return err - } - - // Construct network create request body - nc := types.NetworkCreate{ - Driver: opts.driver, - Options: opts.driverOpts.GetAll(), - IPAM: &network.IPAM{ - Driver: opts.ipamDriver, - Config: ipamCfg, - Options: opts.ipamOpt.GetAll(), - }, - CheckDuplicate: true, - Internal: opts.internal, - EnableIPv6: opts.ipv6, - Attachable: opts.attachable, - Ingress: opts.ingress, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - } - - resp, err := client.NetworkCreate(context.Background(), opts.name, nc) - if err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "%s\n", resp.ID) - return nil -} - -// Consolidates the ipam configuration as a group from different related configurations -// user can configure network with multiple non-overlapping subnets and hence it is -// possible to correlate the various related parameters and consolidate them. -// consolidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into -// structured ipam data. -func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { - if len(subnets) < len(ranges) || len(subnets) < len(gateways) { - return nil, errors.Errorf("every ip-range or gateway must have a corresponding subnet") - } - iData := map[string]*network.IPAMConfig{} - - // Populate non-overlapping subnets into consolidation map - for _, s := range subnets { - for k := range iData { - ok1, err := subnetMatches(s, k) - if err != nil { - return nil, err - } - ok2, err := subnetMatches(k, s) - if err != nil { - return nil, err - } - if ok1 || ok2 { - return nil, errors.Errorf("multiple overlapping subnet configuration is not supported") - } - } - iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} - } - - // Validate and add valid ip ranges - for _, r := range ranges { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, r) - if err != nil { - return nil, err - } - if !ok { - continue - } - if iData[s].IPRange != "" { - return nil, errors.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) - } - d := iData[s] - d.IPRange = r - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for range %s", r) - } - } - - // Validate and add valid gateways - for _, g := range gateways { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, g) - if err != nil { - return nil, err - } - if !ok { - continue - } - if iData[s].Gateway != "" { - return nil, errors.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) - } - d := iData[s] - d.Gateway = g - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for gateway %s", g) - } - } - - // Validate and add aux-addresses - for key, aa := range auxaddrs { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, aa) - if err != nil { - return nil, err - } - if !ok { - continue - } - iData[s].AuxAddress[key] = aa - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for aux-address %s", aa) - } - } - - idl := []network.IPAMConfig{} - for _, v := range iData { - idl = append(idl, *v) - } - return idl, nil -} - -func subnetMatches(subnet, data string) (bool, error) { - var ( - ip net.IP - ) - - _, s, err := net.ParseCIDR(subnet) - if err != nil { - return false, errors.Errorf("Invalid subnet %s : %v", s, err) - } - - if strings.Contains(data, "/") { - ip, _, err = net.ParseCIDR(data) - if err != nil { - return false, errors.Errorf("Invalid cidr %s : %v", data, err) - } - } else { - ip = net.ParseIP(data) - } - - return s.Contains(ip), nil -} diff --git a/cli/command/network/disconnect.go b/cli/command/network/disconnect.go deleted file mode 100644 index c9d9c14a13..0000000000 --- a/cli/command/network/disconnect.go +++ /dev/null @@ -1,41 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type disconnectOptions struct { - network string - container string - force bool -} - -func newDisconnectCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := disconnectOptions{} - - cmd := &cobra.Command{ - Use: "disconnect [OPTIONS] NETWORK CONTAINER", - Short: "Disconnect a container from a network", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.network = args[0] - opts.container = args[1] - return runDisconnect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force the container to disconnect from a network") - - return cmd -} - -func runDisconnect(dockerCli *command.DockerCli, opts disconnectOptions) error { - client := dockerCli.Client() - - return client.NetworkDisconnect(context.Background(), opts.network, opts.container, opts.force) -} diff --git a/cli/command/network/inspect.go b/cli/command/network/inspect.go deleted file mode 100644 index e58d66b77a..0000000000 --- a/cli/command/network/inspect.go +++ /dev/null @@ -1,47 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - format string - names []string - verbose bool -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] NETWORK [NETWORK...]", - Short: "Display detailed information on one or more networks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "Verbose output for diagnostics") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - - ctx := context.Background() - - getNetFunc := func(name string) (interface{}, []byte, error) { - return client.NetworkInspectWithRaw(ctx, name, opts.verbose) - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc) -} diff --git a/cli/command/network/list.go b/cli/command/network/list.go deleted file mode 100644 index 1a5d285103..0000000000 --- a/cli/command/network/list.go +++ /dev/null @@ -1,76 +0,0 @@ -package network - -import ( - "sort" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type byNetworkName []types.NetworkResource - -func (r byNetworkName) Len() int { return len(r) } -func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name } - -type listOptions struct { - quiet bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List networks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") - flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'driver=bridge')") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - client := dockerCli.Client() - options := types.NetworkListOptions{Filters: opts.filter.Value()} - networkResources, err := client.NetworkList(context.Background(), options) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().NetworksFormat - } else { - format = formatter.TableFormatKey - } - } - - sort.Sort(byNetworkName(networkResources)) - - networksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNetworkFormat(format, opts.quiet), - Trunc: !opts.noTrunc, - } - return formatter.NetworkWrite(networksCtx, networkResources) -} diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go deleted file mode 100644 index ec363ab914..0000000000 --- a/cli/command/network/prune.go +++ /dev/null @@ -1,77 +0,0 @@ -package network - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for networks -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all unused networks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const warning = `WARNING! This will remove all networks not used by at least one container. -Are you sure you want to continue?` - -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { - pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) - - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.NetworksDeleted) > 0 { - output = "Deleted Networks:\n" - for _, id := range report.NetworksDeleted { - output += id + "\n" - } - } - - return -} - -// RunPrune calls the Network Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter}) - return 0, output, err -} diff --git a/cli/command/network/remove.go b/cli/command/network/remove.go deleted file mode 100644 index b5f074a981..0000000000 --- a/cli/command/network/remove.go +++ /dev/null @@ -1,53 +0,0 @@ -package network - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ - Use: "rm NETWORK [NETWORK...]", - Aliases: []string{"remove"}, - Short: "Remove one or more networks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args) - }, - } -} - -const ingressWarning = "WARNING! Before removing the routing-mesh network, " + - "make sure all the nodes in your swarm run the same docker engine version. " + - "Otherwise, removal may not be effective and functionality of newly create " + - "ingress networks will be impaired.\nAre you sure you want to continue?" - -func runRemove(dockerCli *command.DockerCli, networks []string) error { - client := dockerCli.Client() - ctx := context.Background() - status := 0 - - for _, name := range networks { - if nw, _, err := client.NetworkInspectWithRaw(ctx, name, false); err == nil && - nw.Ingress && - !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) { - continue - } - if err := client.NetworkRemove(ctx, name); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) - status = 1 - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", name) - } - - if status != 0 { - return cli.StatusError{StatusCode: status} - } - return nil -} diff --git a/cli/command/node/client_test.go b/cli/command/node/client_test.go deleted file mode 100644 index 1f5cdc7cee..0000000000 --- a/cli/command/node/client_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package node - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeListFunc func() ([]swarm.Node, error) - nodeRemoveFunc func() error - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc() - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { - if cli.nodeListFunc != nil { - return cli.nodeListFunc() - } - return []swarm.Node{}, nil -} - -func (cli *fakeClient) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error { - if cli.nodeRemoveFunc != nil { - return cli.nodeRemoveFunc() - } - return nil -} - -func (cli *fakeClient) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if cli.nodeUpdateFunc != nil { - return cli.nodeUpdateFunc(nodeID, version, node) - } - return nil -} - -func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { - if cli.taskInspectFunc != nil { - return cli.taskInspectFunc(taskID) - } - return swarm.Task{}, []byte{}, nil -} - -func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { - if cli.taskListFunc != nil { - return cli.taskListFunc(options) - } - return []swarm.Task{}, nil -} diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go deleted file mode 100644 index ea8b40a9a6..0000000000 --- a/cli/command/node/cmd.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "errors" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - apiclient "github.com/docker/docker/client" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// NewNodeCommand returns a cobra command for `node` subcommands -func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "node", - Short: "Manage Swarm nodes", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newDemoteCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newPromoteCommand(dockerCli), - newRemoveCommand(dockerCli), - newPsCommand(dockerCli), - newUpdateCommand(dockerCli), - ) - return cmd -} - -// Reference returns the reference of a node. The special value "self" for a node -// reference is mapped to the current node, hence the node ID is retrieved using -// the `/info` endpoint. -func Reference(ctx context.Context, client apiclient.APIClient, ref string) (string, error) { - if ref == "self" { - info, err := client.Info(ctx) - if err != nil { - return "", err - } - if info.Swarm.NodeID == "" { - // If there's no node ID in /info, the node probably - // isn't a manager. Call a swarm-specific endpoint to - // get a more specific error message. - _, err = client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return "", err - } - return "", errors.New("node ID not found in /info") - } - return info.Swarm.NodeID, nil - } - return ref, nil -} diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go deleted file mode 100644 index 72ed3ea630..0000000000 --- a/cli/command/node/demote.go +++ /dev/null @@ -1,36 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newDemoteCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "demote NODE [NODE...]", - Short: "Demote one or more nodes from manager in the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runDemote(dockerCli, args) - }, - } -} - -func runDemote(dockerCli command.Cli, nodes []string) error { - demote := func(node *swarm.Node) error { - if node.Spec.Role == swarm.NodeRoleWorker { - fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) - return errNoRoleChange - } - node.Spec.Role = swarm.NodeRoleWorker - return nil - } - success := func(nodeID string) { - fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID) - } - return updateNodes(dockerCli, nodes, demote, success) -} diff --git a/cli/command/node/demote_test.go b/cli/command/node/demote_test.go deleted file mode 100644 index 803b9c229b..0000000000 --- a/cli/command/node/demote_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodeDemoteErrors(t *testing.T) { - testCases := []struct { - args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeDemoteNoChange(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.Errorf("expected role worker, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - assert.NoError(t, cmd.Execute()) -} - -func TestNodeDemoteMultipleNode(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.Errorf("expected role worker, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go deleted file mode 100644 index 39b90bb72e..0000000000 --- a/cli/command/node/inspect.go +++ /dev/null @@ -1,72 +0,0 @@ -package node - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - nodeIds []string - format string - pretty bool -} - -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] self|NODE [NODE...]", - Short: "Display detailed information on one or more nodes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.nodeIds = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.pretty { - opts.format = "pretty" - } - - getRef := func(ref string) (interface{}, []byte, error) { - nodeRef, err := Reference(ctx, client, ref) - if err != nil { - return nil, nil, err - } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - return node, nil, err - } - f := opts.format - - // check if the user is trying to apply a template to the pretty format, which - // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { - return fmt.Errorf("Cannot supply extra formatting options to the pretty template") - } - - nodeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNodeFormat(f, false), - } - - if err := formatter.NodeInspectWrite(nodeCtx, opts.nodeIds, getRef); err != nil { - return cli.StatusError{StatusCode: 1, Status: err.Error()} - } - return nil -} diff --git a/cli/command/node/inspect_test.go b/cli/command/node/inspect_test.go deleted file mode 100644 index 95b45d51e9..0000000000 --- a/cli/command/node/inspect_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package node - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNodeInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - infoFunc func() (types.Info, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"self"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"self"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - infoFunc: func() (types.Info, error) { - return types.Info{Swarm: swarm.Info{NodeID: "abc"}}, nil - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"self"}, - flags: map[string]string{ - "pretty": "true", - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeInspectPretty(t *testing.T) { - testCases := []struct { - name string - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "simple", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "lbl1": "value1", - })), []byte{}, nil - }, - }, - { - name: "manager", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - }, - { - name: "manager-leader", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager(Leader())), []byte{}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - cmd.Flags().Set("pretty", "true") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/node/list.go b/cli/command/node/list.go deleted file mode 100644 index 9c6224dd19..0000000000 --- a/cli/command/node/list.go +++ /dev/null @@ -1,73 +0,0 @@ -package node - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List nodes in the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print nodes using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - nodes, err := client.NodeList( - ctx, - types.NodeListOptions{Filters: opts.filter.Value()}) - if err != nil { - return err - } - - info := types.Info{} - if len(nodes) > 0 && !opts.quiet { - // only non-empty nodes and not quiet, should we call /info api - info, err = client.Info(ctx) - if err != nil { - return err - } - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - if len(dockerCli.ConfigFile().NodesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().NodesFormat - } - } - - nodesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNodeFormat(format, opts.quiet), - } - return formatter.NodeWrite(nodesCtx, nodes, info) -} diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go deleted file mode 100644 index af2d6be156..0000000000 --- a/cli/command/node/list_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/stretchr/testify/assert" -) - -func TestNodeListErrorOnAPIFailure(t *testing.T) { - testCases := []struct { - nodeListFunc func() ([]swarm.Node, error) - infoFunc func() (types.Info, error) - expectedError string - }{ - { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{}, errors.Errorf("error listing nodes") - }, - expectedError: "error listing nodes", - }, - { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - { - ID: "nodeID", - }, - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: tc.nodeListFunc, - infoFunc: tc.infoFunc, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeList(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) - assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) - assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) -} - -func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.Flags().Set("quiet", "true") - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "nodeID") -} - -// Test case for #24090 -func TestNodeListContainsHostname(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{}, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "HOSTNAME") -} - -func TestNodeListDefaultFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", - }) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1: nodeHostname1 Ready/Leader`) - assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`) - assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`) -} - -func TestNodeListFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", - }) - cmd := newListCommand(cli) - cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}") - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeHostname1: Leader`) - assert.Contains(t, buf.String(), `nodeHostname2: Reachable`) -} diff --git a/cli/command/node/opts.go b/cli/command/node/opts.go deleted file mode 100644 index 0ad365f0c6..0000000000 --- a/cli/command/node/opts.go +++ /dev/null @@ -1,24 +0,0 @@ -package node - -import ( - "github.com/docker/docker/opts" -) - -type nodeOptions struct { - annotations - role string - availability string -} - -type annotations struct { - name string - labels opts.ListOpts -} - -func newNodeOptions() *nodeOptions { - return &nodeOptions{ - annotations: annotations{ - labels: opts.NewListOpts(nil), - }, - } -} diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go deleted file mode 100644 index 94fff6400b..0000000000 --- a/cli/command/node/promote.go +++ /dev/null @@ -1,36 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newPromoteCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "promote NODE [NODE...]", - Short: "Promote one or more nodes to manager in the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPromote(dockerCli, args) - }, - } -} - -func runPromote(dockerCli command.Cli, nodes []string) error { - promote := func(node *swarm.Node) error { - if node.Spec.Role == swarm.NodeRoleManager { - fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) - return errNoRoleChange - } - node.Spec.Role = swarm.NodeRoleManager - return nil - } - success := func(nodeID string) { - fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) - } - return updateNodes(dockerCli, nodes, promote, success) -} diff --git a/cli/command/node/promote_test.go b/cli/command/node/promote_test.go deleted file mode 100644 index ce2fb13dca..0000000000 --- a/cli/command/node/promote_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodePromoteErrors(t *testing.T) { - testCases := []struct { - args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodePromoteNoChange(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - assert.NoError(t, cmd.Execute()) -} - -func TestNodePromoteMultipleNode(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go deleted file mode 100644 index 0ab1c0b9f4..0000000000 --- a/cli/command/node/ps.go +++ /dev/null @@ -1,109 +0,0 @@ -package node - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type psOptions struct { - nodeIDs []string - noResolve bool - noTrunc bool - quiet bool - format string - filter opts.FilterOpt -} - -func newPsCommand(dockerCli command.Cli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] [NODE...]", - Short: "List tasks running on one or more nodes, defaults to current node", - Args: cli.RequiresMinArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - opts.nodeIDs = []string{"self"} - - if len(args) != 0 { - opts.nodeIDs = args - } - - return runPs(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - - return cmd -} - -func runPs(dockerCli command.Cli, opts psOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var ( - errs []string - tasks []swarm.Task - ) - - for _, nodeID := range opts.nodeIDs { - nodeRef, err := Reference(ctx, client, nodeID) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - filter := opts.filter.Value() - filter.Add("node", node.ID) - - nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - tasks = append(tasks, nodeTasks...) - } - - format := opts.format - if len(format) == 0 { - if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - if len(errs) == 0 || len(tasks) != 0 { - if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil { - errs = append(errs, err.Error()) - } - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go deleted file mode 100644 index f604628991..0000000000 --- a/cli/command/node/ps_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package node - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNodePsErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - expectedError string - }{ - { - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{}, errors.Errorf("error returning the task list") - }, - expectedError: "error returning the task list", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPsCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, - taskListFunc: tc.taskListFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodePs(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - }{ - { - name: "simple", - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *Task(WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), PortStatus([]swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 80, - Protocol: "tcp", - }, - }))), - }, nil - }, - }, - { - name: "with-errors", - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *Task(TaskID("taskID1"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID2"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID3"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))), - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPsCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, - taskListFunc: tc.taskListFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/node/remove.go b/cli/command/node/remove.go deleted file mode 100644 index bd429ee45f..0000000000 --- a/cli/command/node/remove.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type removeOptions struct { - force bool -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - opts := removeOptions{} - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] NODE [NODE...]", - Aliases: []string{"remove"}, - Short: "Remove one or more nodes from the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm") - return cmd -} - -func runRemove(dockerCli command.Cli, args []string, opts removeOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - - for _, nodeID := range args { - err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force}) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/node/remove_test.go b/cli/command/node/remove_test.go deleted file mode 100644 index b53431dfa7..0000000000 --- a/cli/command/node/remove_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNodeRemoveErrors(t *testing.T) { - testCases := []struct { - args []string - nodeRemoveFunc func() error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeRemoveFunc: func() error { - return errors.Errorf("error removing the node") - }, - expectedError: "error removing the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newRemoveCommand( - test.NewFakeCli(&fakeClient{ - nodeRemoveFunc: tc.nodeRemoveFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeRemoveMultiple(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden b/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden deleted file mode 100644 index 461fc46ea2..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden +++ /dev/null @@ -1,25 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Manager Status: - Address: 127.0.0.1 - Raft Status: Reachable - Leader: Yes -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-inspect-pretty.manager.golden b/cli/command/node/testdata/node-inspect-pretty.manager.golden deleted file mode 100644 index 2c660188d5..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.manager.golden +++ /dev/null @@ -1,25 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Manager Status: - Address: 127.0.0.1 - Raft Status: Reachable - Leader: No -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-inspect-pretty.simple.golden b/cli/command/node/testdata/node-inspect-pretty.simple.golden deleted file mode 100644 index e63bc12596..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.simple.golden +++ /dev/null @@ -1,23 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Labels: - - lbl1 = value1 -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-ps.simple.golden b/cli/command/node/testdata/node-ps.simple.golden deleted file mode 100644 index f9555d8792..0000000000 --- a/cli/command/node/testdata/node-ps.simple.golden +++ /dev/null @@ -1,2 +0,0 @@ -ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp diff --git a/cli/command/node/testdata/node-ps.with-errors.golden b/cli/command/node/testdata/node-ps.with-errors.golden deleted file mode 100644 index 273b30fa11..0000000000 --- a/cli/command/node/testdata/node-ps.with-errors.golden +++ /dev/null @@ -1,4 +0,0 @@ -ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error" -taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error" -taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error" diff --git a/cli/command/node/update.go b/cli/command/node/update.go deleted file mode 100644 index 82668595a7..0000000000 --- a/cli/command/node/update.go +++ /dev/null @@ -1,121 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -var ( - errNoRoleChange = errors.New("role was already set to the requested value") -) - -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { - nodeOpts := newNodeOptions() - - cmd := &cobra.Command{ - Use: "update [OPTIONS] NODE", - Short: "Update a node", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), args[0]) - }, - } - - flags := cmd.Flags() - flags.StringVar(&nodeOpts.role, flagRole, "", `Role of the node ("worker"|"manager")`) - flags.StringVar(&nodeOpts.availability, flagAvailability, "", `Availability of the node ("active"|"pause"|"drain")`) - flags.Var(&nodeOpts.annotations.labels, flagLabelAdd, "Add or update a node label (key=value)") - labelKeys := opts.NewListOpts(nil) - flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists") - return cmd -} - -func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { - success := func(_ string) { - fmt.Fprintln(dockerCli.Out(), nodeID) - } - return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) -} - -func updateNodes(dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { - client := dockerCli.Client() - ctx := context.Background() - - for _, nodeID := range nodes { - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) - if err != nil { - return err - } - - err = mergeNode(&node) - if err != nil { - if err == errNoRoleChange { - continue - } - return err - } - err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec) - if err != nil { - return err - } - success(nodeID) - } - return nil -} - -func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error { - return func(node *swarm.Node) error { - spec := &node.Spec - - if flags.Changed(flagRole) { - str, err := flags.GetString(flagRole) - if err != nil { - return err - } - spec.Role = swarm.NodeRole(str) - } - if flags.Changed(flagAvailability) { - str, err := flags.GetString(flagAvailability) - if err != nil { - return err - } - spec.Availability = swarm.NodeAvailability(str) - } - if spec.Annotations.Labels == nil { - spec.Annotations.Labels = make(map[string]string) - } - if flags.Changed(flagLabelAdd) { - labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() - for k, v := range runconfigopts.ConvertKVStringsToMap(labels) { - spec.Annotations.Labels[k] = v - } - } - if flags.Changed(flagLabelRemove) { - keys := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, k := range keys { - // if a key doesn't exist, fail the command explicitly - if _, exists := spec.Annotations.Labels[k]; !exists { - return errors.Errorf("key %s doesn't exist in node's labels", k) - } - delete(spec.Annotations.Labels, k) - } - } - return nil - } -} - -const ( - flagRole = "role" - flagAvailability = "availability" - flagLabelAdd = "label-add" - flagLabelRemove = "label-rm" -) diff --git a/cli/command/node/update_test.go b/cli/command/node/update_test.go deleted file mode 100644 index a5e2d20e9a..0000000000 --- a/cli/command/node/update_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodeUpdateErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires exactly 1 argument", - }, - { - args: []string{"node1", "node2"}, - expectedError: "requires exactly 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil - }, - flags: map[string]string{ - "label-rm": "notpresent", - }, - expectedError: "key notpresent doesn't exist in node's labels", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeUpdate(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - }{ - { - args: []string{"nodeID"}, - flags: map[string]string{ - "role": "manager", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "availability": "drain", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Availability != swarm.NodeAvailabilityDrain { - return errors.Errorf("expected drain availability, got %s", node.Availability) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-add": "lbl", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if _, present := node.Annotations.Labels["lbl"]; !present { - return errors.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-add": "key=value", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if value, present := node.Annotations.Labels["key"]; !present || value != "value" { - return errors.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-rm": "key", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if len(node.Annotations.Labels) > 0 { - return errors.Errorf("expected no labels, got %v", node.Annotations.Labels) - } - return nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/out.go b/cli/command/out.go deleted file mode 100644 index 27b44c235d..0000000000 --- a/cli/command/out.go +++ /dev/null @@ -1,50 +0,0 @@ -package command - -import ( - "io" - "os" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/term" -) - -// OutStream is an output stream used by the DockerCli to write normal program -// output. -type OutStream struct { - CommonStream - out io.Writer -} - -func (o *OutStream) Write(p []byte) (int, error) { - return o.out.Write(p) -} - -// SetRawTerminal sets raw mode on the input terminal -func (o *OutStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal { - return nil - } - o.CommonStream.state, err = term.SetRawTerminalOutput(o.CommonStream.fd) - return err -} - -// GetTtySize returns the height and width in characters of the tty -func (o *OutStream) GetTtySize() (uint, uint) { - if !o.isTerminal { - return 0, 0 - } - ws, err := term.GetWinsize(o.fd) - if err != nil { - logrus.Debugf("Error getting size: %s", err) - if ws == nil { - return 0, 0 - } - } - return uint(ws.Height), uint(ws.Width) -} - -// NewOutStream returns a new OutStream object from a Writer -func NewOutStream(out io.Writer) *OutStream { - fd, isTerminal := term.GetFdInfo(out) - return &OutStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, out: out} -} diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go deleted file mode 100644 index 33046d2cb8..0000000000 --- a/cli/command/plugin/cmd.go +++ /dev/null @@ -1,32 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "plugin", - Short: "Manage plugins", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - - cmd.AddCommand( - newDisableCommand(dockerCli), - newEnableCommand(dockerCli), - newInspectCommand(dockerCli), - newInstallCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newSetCommand(dockerCli), - newPushCommand(dockerCli), - newCreateCommand(dockerCli), - newUpgradeCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/plugin/create.go b/cli/command/plugin/create.go deleted file mode 100644 index b51f1933db..0000000000 --- a/cli/command/plugin/create.go +++ /dev/null @@ -1,128 +0,0 @@ -package plugin - -import ( - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/archive" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// validateTag checks if the given repoName can be resolved. -func validateTag(rawRepo string) error { - _, err := reference.ParseNormalizedNamed(rawRepo) - - return err -} - -// validateConfig ensures that a valid config.json is available in the given path -func validateConfig(path string) error { - dt, err := os.Open(filepath.Join(path, "config.json")) - if err != nil { - return err - } - - m := types.PluginConfig{} - err = json.NewDecoder(dt).Decode(&m) - dt.Close() - - return err -} - -// validateContextDir validates the given dir and returns abs path on success. -func validateContextDir(contextDir string) (string, error) { - absContextDir, err := filepath.Abs(contextDir) - if err != nil { - return "", err - } - stat, err := os.Lstat(absContextDir) - if err != nil { - return "", err - } - - if !stat.IsDir() { - return "", errors.Errorf("context must be a directory") - } - - return absContextDir, nil -} - -type pluginCreateOptions struct { - repoName string - context string - compress bool -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - options := pluginCreateOptions{} - - cmd := &cobra.Command{ - Use: "create [OPTIONS] PLUGIN PLUGIN-DATA-DIR", - Short: "Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - options.repoName = args[0] - options.context = args[1] - return runCreate(dockerCli, options) - }, - } - - flags := cmd.Flags() - - flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error { - var ( - createCtx io.ReadCloser - err error - ) - - if err := validateTag(options.repoName); err != nil { - return err - } - - absContextDir, err := validateContextDir(options.context) - if err != nil { - return err - } - - if err := validateConfig(options.context); err != nil { - return err - } - - compression := archive.Uncompressed - if options.compress { - logrus.Debugf("compression enabled") - compression = archive.Gzip - } - - createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{ - Compression: compression, - }) - - if err != nil { - return err - } - - ctx := context.Background() - - createOptions := types.PluginCreateOptions{RepoName: options.repoName} - if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), options.repoName) - return nil -} diff --git a/cli/command/plugin/disable.go b/cli/command/plugin/disable.go deleted file mode 100644 index 07b0ec2288..0000000000 --- a/cli/command/plugin/disable.go +++ /dev/null @@ -1,36 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { - var force bool - - cmd := &cobra.Command{ - Use: "disable [OPTIONS] PLUGIN", - Short: "Disable a plugin", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runDisable(dockerCli, args[0], force) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&force, "force", "f", false, "Force the disable of an active plugin") - return cmd -} - -func runDisable(dockerCli *command.DockerCli, name string, force bool) error { - if err := dockerCli.Client().PluginDisable(context.Background(), name, types.PluginDisableOptions{Force: force}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/enable.go b/cli/command/plugin/enable.go deleted file mode 100644 index b1ca48f8f1..0000000000 --- a/cli/command/plugin/enable.go +++ /dev/null @@ -1,48 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type enableOpts struct { - timeout int - name string -} - -func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts enableOpts - - cmd := &cobra.Command{ - Use: "enable [OPTIONS] PLUGIN", - Short: "Enable a plugin", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runEnable(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVar(&opts.timeout, "timeout", 0, "HTTP client timeout (in seconds)") - return cmd -} - -func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error { - name := opts.name - if opts.timeout < 0 { - return errors.Errorf("negative timeout %d is invalid", opts.timeout) - } - - if err := dockerCli.Client().PluginEnable(context.Background(), name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go deleted file mode 100644 index c2c7a0d6bc..0000000000 --- a/cli/command/plugin/inspect.go +++ /dev/null @@ -1,42 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - pluginNames []string - format string -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", - Short: "Display detailed information on one or more plugins", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.pluginNames = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - getRef := func(ref string) (interface{}, []byte, error) { - return client.PluginInspectWithRaw(ctx, ref) - } - - return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) -} diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go deleted file mode 100644 index 18b3fa3739..0000000000 --- a/cli/command/plugin/install.go +++ /dev/null @@ -1,168 +0,0 @@ -package plugin - -import ( - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type pluginOptions struct { - remote string - localName string - grantPerms bool - disable bool - args []string - skipRemoteCheck bool -} - -func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) { - flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") - command.AddTrustVerificationFlags(flags) -} - -func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { - var options pluginOptions - cmd := &cobra.Command{ - Use: "install [OPTIONS] PLUGIN [KEY=VALUE...]", - Short: "Install a plugin", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.remote = args[0] - if len(args) > 1 { - options.args = args[1:] - } - return runInstall(dockerCli, options) - }, - } - - flags := cmd.Flags() - loadPullFlags(&options, flags) - flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") - flags.StringVar(&options.localName, "alias", "", "Local name for plugin") - return cmd -} - -type pluginRegistryService struct { - registry.Service -} - -func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) { - repoInfo, err = s.Service.ResolveRepository(name) - if repoInfo != nil { - repoInfo.Class = "plugin" - } - return -} - -func newRegistryService() registry.Service { - return pluginRegistryService{ - Service: registry.NewService(registry.ServiceOptions{V2Only: true}), - } -} - -func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) { - // Names with both tag and digest will be treated by the daemon - // as a pull by digest with a local name for the tag - // (if no local name is provided). - ref, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return types.PluginInstallOptions{}, err - } - - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return types.PluginInstallOptions{}, err - } - - remote := ref.String() - - _, isCanonical := ref.(reference.Canonical) - if command.IsTrusted() && !isCanonical { - ref = reference.TagNameOnly(ref) - nt, ok := ref.(reference.NamedTagged) - if !ok { - return types.PluginInstallOptions{}, errors.Errorf("invalid name: %s", ref.String()) - } - - ctx := context.Background() - trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) - if err != nil { - return types.PluginInstallOptions{}, err - } - remote = reference.FamiliarString(trusted) - } - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return types.PluginInstallOptions{}, err - } - registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName) - - options := types.PluginInstallOptions{ - RegistryAuth: encodedAuth, - RemoteRef: remote, - Disabled: opts.disable, - AcceptAllPermissions: opts.grantPerms, - AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote), - // TODO: Rename PrivilegeFunc, it has nothing to do with privileges - PrivilegeFunc: registryAuthFunc, - Args: opts.args, - } - return options, nil -} - -func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { - var localName string - if opts.localName != "" { - aref, err := reference.ParseNormalizedNamed(opts.localName) - if err != nil { - return err - } - if _, ok := aref.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", opts.localName) - } - localName = reference.FamiliarString(reference.TagNameOnly(aref)) - } - - ctx := context.Background() - options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install") - if err != nil { - return err - } - responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options) - if err != nil { - if strings.Contains(err.Error(), "(image) when fetching") { - return errors.New(err.Error() + " - Use `docker image pull`") - } - return err - } - defer responseBody.Close() - if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result - return nil -} - -func acceptPrivileges(dockerCli *command.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) { - return func(privileges types.PluginPrivileges) (bool, error) { - fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name) - for _, privilege := range privileges { - fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value) - } - return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil - } -} diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go deleted file mode 100644 index a1b231f570..0000000000 --- a/cli/command/plugin/list.go +++ /dev/null @@ -1,63 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Short: "List plugins", - Aliases: []string{"list"}, - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'enabled=true')") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - plugins, err := dockerCli.Client().PluginList(context.Background(), opts.filter.Value()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().PluginsFormat - } else { - format = formatter.TableFormatKey - } - } - - pluginsCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewPluginFormat(format, opts.quiet), - Trunc: !opts.noTrunc, - } - return formatter.PluginWrite(pluginsCtx, plugins) -} diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go deleted file mode 100644 index de4f95cce8..0000000000 --- a/cli/command/plugin/push.go +++ /dev/null @@ -1,69 +0,0 @@ -package plugin - -import ( - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "push [OPTIONS] PLUGIN[:TAG]", - Short: "Push a plugin to a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPush(dockerCli, args[0]) - }, - } - - flags := cmd.Flags() - - command.AddTrustSigningFlags(flags) - - return cmd -} - -func runPush(dockerCli *command.DockerCli, name string) error { - named, err := reference.ParseNormalizedNamed(name) - if err != nil { - return err - } - if _, ok := named.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", name) - } - - named = reference.TagNameOnly(named) - - ctx := context.Background() - - repoInfo, err := registry.ParseRepositoryInfo(named) - if err != nil { - return err - } - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(named), encodedAuth) - if err != nil { - return err - } - defer responseBody.Close() - - if command.IsTrusted() { - repoInfo.Class = "plugin" - return image.PushTrustedReference(dockerCli, repoInfo, named, authConfig, responseBody) - } - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/plugin/remove.go b/cli/command/plugin/remove.go deleted file mode 100644 index 9f3aba9a01..0000000000 --- a/cli/command/plugin/remove.go +++ /dev/null @@ -1,55 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type rmOptions struct { - force bool - - plugins []string -} - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts rmOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] PLUGIN [PLUGIN...]", - Short: "Remove one or more plugins", - Aliases: []string{"remove"}, - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.plugins = args - return runRemove(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of an active plugin") - return cmd -} - -func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error { - ctx := context.Background() - - var errs cli.Errors - for _, name := range opts.plugins { - // TODO: pass names to api instead of making multiple api calls - if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { - errs = append(errs, err) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. - if errs != nil { - return errs - } - return nil -} diff --git a/cli/command/plugin/set.go b/cli/command/plugin/set.go deleted file mode 100644 index 52b09fb500..0000000000 --- a/cli/command/plugin/set.go +++ /dev/null @@ -1,22 +0,0 @@ -package plugin - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newSetCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "set PLUGIN KEY=VALUE [KEY=VALUE...]", - Short: "Change settings for a plugin", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return dockerCli.Client().PluginSet(context.Background(), args[0], args[1:]) - }, - } - - return cmd -} diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go deleted file mode 100644 index cbcbe17ece..0000000000 --- a/cli/command/plugin/upgrade.go +++ /dev/null @@ -1,90 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command { - var options pluginOptions - cmd := &cobra.Command{ - Use: "upgrade [OPTIONS] PLUGIN [REMOTE]", - Short: "Upgrade an existing plugin", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - options.localName = args[0] - if len(args) == 2 { - options.remote = args[1] - } - return runUpgrade(dockerCli, options) - }, - Tags: map[string]string{"version": "1.26"}, - } - - flags := cmd.Flags() - loadPullFlags(&options, flags) - flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") - return cmd -} - -func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { - ctx := context.Background() - p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName) - if err != nil { - return errors.Errorf("error reading plugin data: %v", err) - } - - if p.Enabled { - return errors.Errorf("the plugin must be disabled before upgrading") - } - - opts.localName = p.Name - if opts.remote == "" { - opts.remote = p.PluginReference - } - remote, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return errors.Wrap(err, "error parsing remote upgrade image reference") - } - remote = reference.TagNameOnly(remote) - - old, err := reference.ParseNormalizedNamed(p.PluginReference) - if err != nil { - return errors.Wrap(err, "error parsing current image reference") - } - old = reference.TagNameOnly(old) - - fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote)) - if !opts.skipRemoteCheck && remote.String() != old.String() { - if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") { - return errors.New("canceling upgrade request") - } - } - - options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade") - if err != nil { - return err - } - - responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options) - if err != nil { - if strings.Contains(err.Error(), "target is image") { - return errors.New(err.Error() + " - Use `docker image pull`") - } - return err - } - defer responseBody.Close() - if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result - return nil -} diff --git a/cli/command/prune/prune.go b/cli/command/prune/prune.go deleted file mode 100644 index 26153ed7c1..0000000000 --- a/cli/command/prune/prune.go +++ /dev/null @@ -1,51 +0,0 @@ -package prune - -import ( - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/container" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/cli/command/network" - "github.com/docker/docker/cli/command/volume" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -// NewContainerPruneCommand returns a cobra prune command for containers -func NewContainerPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return container.NewPruneCommand(dockerCli) -} - -// NewVolumePruneCommand returns a cobra prune command for volumes -func NewVolumePruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return volume.NewPruneCommand(dockerCli) -} - -// NewImagePruneCommand returns a cobra prune command for images -func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return image.NewPruneCommand(dockerCli) -} - -// NewNetworkPruneCommand returns a cobra prune command for Networks -func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return network.NewPruneCommand(dockerCli) -} - -// RunContainerPrune executes a prune command for containers -func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return container.RunPrune(dockerCli, filter) -} - -// RunVolumePrune executes a prune command for volumes -func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return volume.RunPrune(dockerCli, filter) -} - -// RunImagePrune executes a prune command for images -func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return image.RunPrune(dockerCli, all, filter) -} - -// RunNetworkPrune executes a prune command for networks -func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return network.RunPrune(dockerCli, filter) -} diff --git a/cli/command/registry.go b/cli/command/registry.go deleted file mode 100644 index 884fa6ec40..0000000000 --- a/cli/command/registry.go +++ /dev/null @@ -1,187 +0,0 @@ -package command - -import ( - "bufio" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "os" - "runtime" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/pkg/term" - "github.com/docker/docker/registry" - "github.com/pkg/errors" -) - -// ElectAuthServer returns the default registry to use (by asking the daemon) -func ElectAuthServer(ctx context.Context, cli Cli) string { - // The daemon `/info` endpoint informs us of the default registry being - // used. This is essential in cross-platforms environment, where for - // example a Linux client might be interacting with a Windows daemon, hence - // the default registry URL might be Windows specific. - serverAddress := registry.IndexServer - if info, err := cli.Client().Info(ctx); err != nil { - fmt.Fprintf(cli.Out(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) - } else { - serverAddress = info.IndexServerAddress - } - return serverAddress -} - -// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload -func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { - buf, err := json.Marshal(authConfig) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(buf), nil -} - -// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info -// for the given command. -func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { - return func() (string, error) { - fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) - indexServer := registry.GetAuthConfigKey(index) - isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli) - authConfig, err := ConfigureAuth(cli, "", "", indexServer, isDefaultRegistry) - if err != nil { - return "", err - } - return EncodeAuthToBase64(authConfig) - } -} - -// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the -// default index, it uses the default index name for the daemon's platform, -// not the client's platform. -func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig { - configKey := index.Name - if index.Official { - configKey = ElectAuthServer(ctx, cli) - } - - a, _ := cli.CredentialsStore(configKey).Get(configKey) - return a -} - -// ConfigureAuth returns an AuthConfig from the specified user, password and server. -func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { - // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 - if runtime.GOOS == "windows" { - cli.SetIn(NewInStream(os.Stdin)) - } - - if !isDefaultRegistry { - serverAddress = registry.ConvertToHostname(serverAddress) - } - - authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) - if err != nil { - return authconfig, err - } - - // Some links documenting this: - // - https://code.google.com/archive/p/mintty/issues/56 - // - https://github.com/docker/docker/issues/15272 - // - https://mintty.github.io/ (compatibility) - // Linux will hit this if you attempt `cat | docker login`, and Windows - // will hit this if you attempt docker login from mintty where stdin - // is a pipe, not a character based console. - if flPassword == "" && !cli.In().IsTerminal() { - return authconfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") - } - - authconfig.Username = strings.TrimSpace(authconfig.Username) - - if flUser = strings.TrimSpace(flUser); flUser == "" { - if isDefaultRegistry { - // if this is a default registry (docker hub), then display the following message. - fmt.Fprintln(cli.Out(), "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") - } - promptWithDefault(cli.Out(), "Username", authconfig.Username) - flUser = readInput(cli.In(), cli.Out()) - flUser = strings.TrimSpace(flUser) - if flUser == "" { - flUser = authconfig.Username - } - } - if flUser == "" { - return authconfig, errors.Errorf("Error: Non-null Username Required") - } - if flPassword == "" { - oldState, err := term.SaveState(cli.In().FD()) - if err != nil { - return authconfig, err - } - fmt.Fprintf(cli.Out(), "Password: ") - term.DisableEcho(cli.In().FD(), oldState) - - flPassword = readInput(cli.In(), cli.Out()) - fmt.Fprint(cli.Out(), "\n") - - term.RestoreTerminal(cli.In().FD(), oldState) - if flPassword == "" { - return authconfig, errors.Errorf("Error: Password Required") - } - } - - authconfig.Username = flUser - authconfig.Password = flPassword - authconfig.ServerAddress = serverAddress - authconfig.IdentityToken = "" - - return authconfig, nil -} - -func readInput(in io.Reader, out io.Writer) string { - reader := bufio.NewReader(in) - line, _, err := reader.ReadLine() - if err != nil { - fmt.Fprintln(out, err.Error()) - os.Exit(1) - } - return string(line) -} - -func promptWithDefault(out io.Writer, prompt string, configDefault string) { - if configDefault == "" { - fmt.Fprintf(out, "%s: ", prompt) - } else { - fmt.Fprintf(out, "%s (%s): ", prompt, configDefault) - } -} - -// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image -func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) { - // Retrieve encoded auth token from the image reference - authConfig, err := resolveAuthConfigFromImage(ctx, cli, image) - if err != nil { - return "", err - } - encodedAuth, err := EncodeAuthToBase64(authConfig) - if err != nil { - return "", err - } - return encodedAuth, nil -} - -// resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) { - registryRef, err := reference.ParseNormalizedNamed(image) - if err != nil { - return types.AuthConfig{}, err - } - repoInfo, err := registry.ParseRepositoryInfo(registryRef) - if err != nil { - return types.AuthConfig{}, err - } - return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil -} diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go deleted file mode 100644 index 343d107dc2..0000000000 --- a/cli/command/registry/login.go +++ /dev/null @@ -1,87 +0,0 @@ -package registry - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type loginOptions struct { - serverAddress string - user string - password string - email string -} - -// NewLoginCommand creates a new `docker login` command -func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts loginOptions - - cmd := &cobra.Command{ - Use: "login [OPTIONS] [SERVER]", - Short: "Log in to a Docker registry", - Long: "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.serverAddress = args[0] - } - return runLogin(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.user, "username", "u", "", "Username") - flags.StringVarP(&opts.password, "password", "p", "", "Password") - - // Deprecated in 1.11: Should be removed in docker 17.06 - flags.StringVarP(&opts.email, "email", "e", "", "Email") - flags.MarkDeprecated("email", "will be removed in 17.06.") - - return cmd -} - -func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { - ctx := context.Background() - clnt := dockerCli.Client() - - var ( - serverAddress string - authServer = command.ElectAuthServer(ctx, dockerCli) - ) - if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { - serverAddress = opts.serverAddress - } else { - serverAddress = authServer - } - - isDefaultRegistry := serverAddress == authServer - - authConfig, err := command.ConfigureAuth(dockerCli, opts.user, opts.password, serverAddress, isDefaultRegistry) - if err != nil { - return err - } - response, err := clnt.RegistryLogin(ctx, authConfig) - if err != nil { - return err - } - if response.IdentityToken != "" { - authConfig.Password = "" - authConfig.IdentityToken = response.IdentityToken - } - if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { - return errors.Errorf("Error saving credentials: %v", err) - } - - if response.Status != "" { - fmt.Fprintln(dockerCli.Out(), response.Status) - } - return nil -} diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go deleted file mode 100644 index f1f397fa08..0000000000 --- a/cli/command/registry/logout.go +++ /dev/null @@ -1,77 +0,0 @@ -package registry - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -// NewLogoutCommand creates a new `docker logout` command -func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "logout [SERVER]", - Short: "Log out from a Docker registry", - Long: "Log out from a Docker registry.\nIf no server is specified, the default is defined by the daemon.", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - var serverAddress string - if len(args) > 0 { - serverAddress = args[0] - } - return runLogout(dockerCli, serverAddress) - }, - } - - return cmd -} - -func runLogout(dockerCli *command.DockerCli, serverAddress string) error { - ctx := context.Background() - var isDefaultRegistry bool - - if serverAddress == "" { - serverAddress = command.ElectAuthServer(ctx, dockerCli) - isDefaultRegistry = true - } - - var ( - loggedIn bool - regsToLogout []string - hostnameAddress = serverAddress - regsToTry = []string{serverAddress} - ) - if !isDefaultRegistry { - hostnameAddress = registry.ConvertToHostname(serverAddress) - // the tries below are kept for backward compatibility where a user could have - // saved the registry in one of the following format. - regsToTry = append(regsToTry, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) - } - - // check if we're logged in based on the records in the config file - // which means it couldn't have user/pass cause they may be in the creds store - for _, s := range regsToTry { - if _, ok := dockerCli.ConfigFile().AuthConfigs[s]; ok { - loggedIn = true - regsToLogout = append(regsToLogout, s) - } - } - - if !loggedIn { - fmt.Fprintf(dockerCli.Out(), "Not logged in to %s\n", hostnameAddress) - return nil - } - - fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) - for _, r := range regsToLogout { - if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { - fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) - } - } - - return nil -} diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go deleted file mode 100644 index f534082d32..0000000000 --- a/cli/command/registry/search.go +++ /dev/null @@ -1,126 +0,0 @@ -package registry - -import ( - "fmt" - "sort" - "strings" - "text/tabwriter" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/stringutils" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -type searchOptions struct { - term string - noTrunc bool - limit int - filter opts.FilterOpt - - // Deprecated - stars uint - automated bool -} - -// NewSearchCommand creates a new `docker search` command -func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := searchOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "search [OPTIONS] TERM", - Short: "Search the Docker Hub for images", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.term = args[0] - return runSearch(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results") - - flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") - flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars") - - flags.MarkDeprecated("automated", "use --filter=is-automated=true instead") - flags.MarkDeprecated("stars", "use --filter=stars=3 instead") - - return cmd -} - -func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { - indexInfo, err := registry.ParseSearchIndexInfo(opts.term) - if err != nil { - return err - } - - ctx := context.Background() - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, indexInfo) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, indexInfo, "search") - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageSearchOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - Filters: opts.filter.Value(), - Limit: opts.limit, - } - - clnt := dockerCli.Client() - - unorderedResults, err := clnt.ImageSearch(ctx, opts.term, options) - if err != nil { - return err - } - - results := searchResultsByStars(unorderedResults) - sort.Sort(results) - - w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0) - fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") - for _, res := range results { - // --automated and -s, --stars are deprecated since Docker 1.12 - if (opts.automated && !res.IsAutomated) || (int(opts.stars) > res.StarCount) { - continue - } - desc := strings.Replace(res.Description, "\n", " ", -1) - desc = strings.Replace(desc, "\r", " ", -1) - if !opts.noTrunc { - desc = stringutils.Ellipsis(desc, 45) - } - fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) - if res.IsOfficial { - fmt.Fprint(w, "[OK]") - - } - fmt.Fprint(w, "\t") - if res.IsAutomated { - fmt.Fprint(w, "[OK]") - } - fmt.Fprint(w, "\n") - } - w.Flush() - return nil -} - -// searchResultsByStars sorts search results in descending order by number of stars. -type searchResultsByStars []registrytypes.SearchResult - -func (r searchResultsByStars) Len() int { return len(r) } -func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/cli/command/secret/client_test.go b/cli/command/secret/client_test.go deleted file mode 100644 index bb4b412fc2..0000000000 --- a/cli/command/secret/client_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) - secretInspectFunc func(string) (swarm.Secret, []byte, error) - secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) - secretRemoveFunc func(string) error -} - -func (c *fakeClient) SecretCreate(ctx context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if c.secretCreateFunc != nil { - return c.secretCreateFunc(spec) - } - return types.SecretCreateResponse{}, nil -} - -func (c *fakeClient) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { - if c.secretInspectFunc != nil { - return c.secretInspectFunc(id) - } - return swarm.Secret{}, nil, nil -} - -func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - if c.secretListFunc != nil { - return c.secretListFunc(options) - } - return []swarm.Secret{}, nil -} - -func (c *fakeClient) SecretRemove(ctx context.Context, name string) error { - if c.secretRemoveFunc != nil { - return c.secretRemoveFunc(name) - } - return nil -} diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go deleted file mode 100644 index acaef4dcac..0000000000 --- a/cli/command/secret/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package secret - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSecretCommand returns a cobra command for `secret` subcommands -func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "secret", - Short: "Manage Docker secrets", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - cmd.AddCommand( - newSecretListCommand(dockerCli), - newSecretCreateCommand(dockerCli), - newSecretInspectCommand(dockerCli), - newSecretRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/secret/create.go b/cli/command/secret/create.go deleted file mode 100644 index 59b0798178..0000000000 --- a/cli/command/secret/create.go +++ /dev/null @@ -1,80 +0,0 @@ -package secret - -import ( - "fmt" - "io" - "io/ioutil" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/system" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type createOptions struct { - name string - file string - labels opts.ListOpts -} - -func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { - createOpts := createOptions{ - labels: opts.NewListOpts(opts.ValidateEnv), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] SECRET file|-", - Short: "Create a secret from a file or STDIN as content", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - createOpts.name = args[0] - createOpts.file = args[1] - return runSecretCreate(dockerCli, createOpts) - }, - } - flags := cmd.Flags() - flags.VarP(&createOpts.labels, "label", "l", "Secret labels") - - return cmd -} - -func runSecretCreate(dockerCli command.Cli, options createOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var in io.Reader = dockerCli.In() - if options.file != "-" { - file, err := system.OpenSequential(options.file) - if err != nil { - return err - } - in = file - defer file.Close() - } - - secretData, err := ioutil.ReadAll(in) - if err != nil { - return errors.Errorf("Error reading content from %q: %v", options.file, err) - } - - spec := swarm.SecretSpec{ - Annotations: swarm.Annotations{ - Name: options.name, - Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), - }, - Data: secretData, - } - - r, err := client.SecretCreate(ctx, spec) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), r.ID) - return nil -} diff --git a/cli/command/secret/create_test.go b/cli/command/secret/create_test.go deleted file mode 100644 index 0e9c1cd4af..0000000000 --- a/cli/command/secret/create_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "path/filepath" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -const secretDataFile = "secret-create-with-name.golden" - -func TestSecretCreateErrors(t *testing.T) { - testCases := []struct { - args []string - secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) - expectedError string - }{ - { - args: []string{"too_few"}, - expectedError: "requires exactly 2 argument(s)", - }, - {args: []string{"too", "many", "arguments"}, - expectedError: "requires exactly 2 argument(s)", - }, - { - args: []string{"name", filepath.Join("testdata", secretDataFile)}, - secretCreateFunc: func(secretSpec swarm.SecretSpec) (types.SecretCreateResponse, error) { - return types.SecretCreateResponse{}, errors.Errorf("error creating secret") - }, - expectedError: "error creating secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretCreateCommand( - test.NewFakeCli(&fakeClient{ - secretCreateFunc: tc.secretCreateFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretCreateWithName(t *testing.T) { - name := "foo" - buf := new(bytes.Buffer) - var actual []byte - cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if spec.Name != name { - return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) - } - - actual = spec.Data - - return types.SecretCreateResponse{ - ID: "ID-" + spec.Name, - }, nil - }, - }, buf) - - cmd := newSecretCreateCommand(cli) - cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) - assert.NoError(t, cmd.Execute()) - expected := golden.Get(t, actual, secretDataFile) - assert.Equal(t, expected, actual) - assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String())) -} - -func TestSecretCreateWithLabels(t *testing.T) { - expectedLabels := map[string]string{ - "lbl1": "Label-foo", - "lbl2": "Label-bar", - } - name := "foo" - - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if spec.Name != name { - return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) - } - - if !compareMap(spec.Labels, expectedLabels) { - return types.SecretCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels) - } - - return types.SecretCreateResponse{ - ID: "ID-" + spec.Name, - }, nil - }, - }, buf) - - cmd := newSecretCreateCommand(cli) - cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) - cmd.Flags().Set("label", "lbl1=Label-foo") - cmd.Flags().Set("label", "lbl2=Label-bar") - assert.NoError(t, cmd.Execute()) - assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String())) -} - -func compareMap(actual map[string]string, expected map[string]string) bool { - if len(actual) != len(expected) { - return false - } - for key, value := range actual { - if expectedValue, ok := expected[key]; ok { - if expectedValue != value { - return false - } - } else { - return false - } - } - return true -} diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go deleted file mode 100644 index 8b3c3c682e..0000000000 --- a/cli/command/secret/inspect.go +++ /dev/null @@ -1,41 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - names []string - format string -} - -func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { - opts := inspectOptions{} - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] SECRET [SECRET...]", - Short: "Display detailed information on one or more secrets", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runSecretInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runSecretInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRef := func(id string) (interface{}, []byte, error) { - return client.SecretInspectWithRaw(ctx, id) - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef) -} diff --git a/cli/command/secret/inspect_test.go b/cli/command/secret/inspect_test.go deleted file mode 100644 index 52b9a1cef0..0000000000 --- a/cli/command/secret/inspect_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package secret - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSecretInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"foo"}, - secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") - }, - expectedError: "error while inspecting the secret", - }, - { - args: []string{"foo"}, - flags: map[string]string{ - "format": "{{invalid format}}", - }, - expectedError: "Template parsing error", - }, - { - args: []string{"foo", "bar"}, - secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { - if secretID == "foo" { - return *Secret(SecretName("foo")), nil, nil - } - return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") - }, - expectedError: "error while inspecting the secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretInspectWithoutFormat(t *testing.T) { - testCases := []struct { - name string - args []string - secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) - }{ - { - name: "single-secret", - args: []string{"foo"}, - secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { - if name != "foo" { - return swarm.Secret{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name) - } - return *Secret(SecretID("ID-foo"), SecretName("foo")), nil, nil - }, - }, - { - name: "multiple-secrets-with-labels", - args: []string{"foo", "bar"}, - secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { - return *Secret(SecretID("ID-"+name), SecretName(name), SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} - -func TestSecretInspectWithFormat(t *testing.T) { - secretInspectFunc := func(name string) (swarm.Secret, []byte, error) { - return *Secret(SecretName("foo"), SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil - } - testCases := []struct { - name string - format string - args []string - secretInspectFunc func(name string) (swarm.Secret, []byte, error) - }{ - { - name: "simple-template", - format: "{{.Spec.Name}}", - args: []string{"foo"}, - secretInspectFunc: secretInspectFunc, - }, - { - name: "json-template", - format: "{{json .Spec.Labels}}", - args: []string{"foo"}, - secretInspectFunc: secretInspectFunc, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.Flags().Set("format", tc.format) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go deleted file mode 100644 index 384ee26509..0000000000 --- a/cli/command/secret/ls.go +++ /dev/null @@ -1,61 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newSecretListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List secrets", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runSecretList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVarP(&opts.format, "format", "", "", "Pretty-print secrets using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runSecretList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - secrets, err := client.SecretList(ctx, types.SecretListOptions{Filters: opts.filter.Value()}) - if err != nil { - return err - } - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().SecretFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().SecretFormat - } else { - format = formatter.TableFormatKey - } - } - secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewSecretFormat(format, opts.quiet), - } - return formatter.SecretWrite(secretCtx, secrets) -} diff --git a/cli/command/secret/ls_test.go b/cli/command/secret/ls_test.go deleted file mode 100644 index cb0510adff..0000000000 --- a/cli/command/secret/ls_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSecretListErrors(t *testing.T) { - testCases := []struct { - args []string - secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) - expectedError string - }{ - { - args: []string{"foo"}, - expectedError: "accepts no argument", - }, - { - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{}, errors.Errorf("error listing secrets") - }, - expectedError: "error listing secrets", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretListCommand( - test.NewFakeCli(&fakeClient{ - secretListFunc: tc.secretListFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretList(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), - SecretName("foo"), - SecretVersion(swarm.Version{Index: 10}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *Secret(SecretID("ID-bar"), - SecretName("bar"), - SecretVersion(swarm.Version{Index: 11}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.SetOutput(buf) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithQuietOption(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("quiet", "true") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-quiet-option.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithConfigFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - SecretFormat: "{{ .Name }} {{ .Labels }}", - }) - cmd := newSecretListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-config-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithFilter(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - assert.Equal(t, "foo", options.Filters.Get("name")[0], "foo") - assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0]) - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), - SecretName("foo"), - SecretVersion(swarm.Version{Index: 10}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *Secret(SecretID("ID-bar"), - SecretName("bar"), - SecretVersion(swarm.Version{Index: 11}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("filter", "name=foo") - cmd.Flags().Set("filter", "label=lbl1=Label-bar") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-filter.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} diff --git a/cli/command/secret/remove.go b/cli/command/secret/remove.go deleted file mode 100644 index a4b501d176..0000000000 --- a/cli/command/secret/remove.go +++ /dev/null @@ -1,53 +0,0 @@ -package secret - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type removeOptions struct { - names []string -} - -func newSecretRemoveCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "rm SECRET [SECRET...]", - Aliases: []string{"remove"}, - Short: "Remove one or more secrets", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts := removeOptions{ - names: args, - } - return runSecretRemove(dockerCli, opts) - }, - } -} - -func runSecretRemove(dockerCli command.Cli, opts removeOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - - for _, name := range opts.names { - if err := client.SecretRemove(ctx, name); err != nil { - errs = append(errs, err.Error()) - continue - } - - fmt.Fprintln(dockerCli.Out(), name) - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/secret/remove_test.go b/cli/command/secret/remove_test.go deleted file mode 100644 index b8bbb5e6f5..0000000000 --- a/cli/command/secret/remove_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSecretRemoveErrors(t *testing.T) { - testCases := []struct { - args []string - secretRemoveFunc func(string) error - expectedError string - }{ - { - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - args: []string{"foo"}, - secretRemoveFunc: func(name string) error { - return errors.Errorf("error removing secret") - }, - expectedError: "error removing secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretRemoveCommand( - test.NewFakeCli(&fakeClient{ - secretRemoveFunc: tc.secretRemoveFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretRemoveWithName(t *testing.T) { - names := []string{"foo", "bar"} - buf := new(bytes.Buffer) - var removedSecrets []string - cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(name string) error { - removedSecrets = append(removedSecrets, name) - return nil - }, - }, buf) - cmd := newSecretRemoveCommand(cli) - cmd.SetArgs(names) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n")) - assert.Equal(t, names, removedSecrets) -} - -func TestSecretRemoveContinueAfterError(t *testing.T) { - names := []string{"foo", "bar"} - buf := new(bytes.Buffer) - var removedSecrets []string - - cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(name string) error { - removedSecrets = append(removedSecrets, name) - if name == "foo" { - return errors.Errorf("error removing secret: %s", name) - } - return nil - }, - }, buf) - - cmd := newSecretRemoveCommand(cli) - cmd.SetArgs(names) - assert.EqualError(t, cmd.Execute(), "error removing secret: foo") - assert.Equal(t, names, removedSecrets) -} diff --git a/cli/command/secret/testdata/secret-create-with-name.golden b/cli/command/secret/testdata/secret-create-with-name.golden deleted file mode 100644 index 788642a93a..0000000000 --- a/cli/command/secret/testdata/secret-create-with-name.golden +++ /dev/null @@ -1 +0,0 @@ -secret_foo_bar diff --git a/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden b/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden deleted file mode 100644 index aab678f85d..0000000000 --- a/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden +++ /dev/null @@ -1 +0,0 @@ -{"label1":"label-foo"} diff --git a/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden b/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden deleted file mode 100644 index 257cc5642c..0000000000 --- a/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden +++ /dev/null @@ -1 +0,0 @@ -foo diff --git a/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden b/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden deleted file mode 100644 index 6887c185f1..0000000000 --- a/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "ID": "ID-foo", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "foo", - "Labels": { - "label1": "label-foo" - } - } - }, - { - "ID": "ID-bar", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "bar", - "Labels": { - "label1": "label-foo" - } - } - } -] diff --git a/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden b/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden deleted file mode 100644 index ea42ec6f4f..0000000000 --- a/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "ID": "ID-foo", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "foo", - "Labels": null - } - } -] diff --git a/cli/command/secret/testdata/secret-list-with-config-format.golden b/cli/command/secret/testdata/secret-list-with-config-format.golden deleted file mode 100644 index 9a47538804..0000000000 --- a/cli/command/secret/testdata/secret-list-with-config-format.golden +++ /dev/null @@ -1,2 +0,0 @@ -foo -bar label=label-bar diff --git a/cli/command/secret/testdata/secret-list-with-filter.golden b/cli/command/secret/testdata/secret-list-with-filter.golden deleted file mode 100644 index 29983de8e9..0000000000 --- a/cli/command/secret/testdata/secret-list-with-filter.golden +++ /dev/null @@ -1,3 +0,0 @@ -ID NAME CREATED UPDATED -ID-foo foo 2 hours ago About an hour ago -ID-bar bar 2 hours ago About an hour ago diff --git a/cli/command/secret/testdata/secret-list-with-format.golden b/cli/command/secret/testdata/secret-list-with-format.golden deleted file mode 100644 index 9a47538804..0000000000 --- a/cli/command/secret/testdata/secret-list-with-format.golden +++ /dev/null @@ -1,2 +0,0 @@ -foo -bar label=label-bar diff --git a/cli/command/secret/testdata/secret-list-with-quiet-option.golden b/cli/command/secret/testdata/secret-list-with-quiet-option.golden deleted file mode 100644 index 83fb6e8979..0000000000 --- a/cli/command/secret/testdata/secret-list-with-quiet-option.golden +++ /dev/null @@ -1,2 +0,0 @@ -ID-foo -ID-bar diff --git a/cli/command/secret/testdata/secret-list.golden b/cli/command/secret/testdata/secret-list.golden deleted file mode 100644 index 29983de8e9..0000000000 --- a/cli/command/secret/testdata/secret-list.golden +++ /dev/null @@ -1,3 +0,0 @@ -ID NAME CREATED UPDATED -ID-foo foo 2 hours ago About an hour ago -ID-bar bar 2 hours ago About an hour ago diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go deleted file mode 100644 index 51208b80c2..0000000000 --- a/cli/command/service/cmd.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewServiceCommand returns a cobra command for `service` subcommands -func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "service", - Short: "Manage services", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newPsCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newScaleCommand(dockerCli), - newUpdateCommand(dockerCli), - newLogsCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/service/create.go b/cli/command/service/create.go deleted file mode 100644 index bb2a1fe3b7..0000000000 --- a/cli/command/service/create.go +++ /dev/null @@ -1,118 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := newServiceOptions() - - cmd := &cobra.Command{ - Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Create a new service", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - if len(args) > 1 { - opts.args = args[1:] - } - return runCreate(dockerCli, cmd.Flags(), opts) - }, - } - flags := cmd.Flags() - flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)") - flags.StringVar(&opts.name, flagName, "", "Service name") - - addServiceFlags(flags, opts, buildServiceDefaultFlagMapping()) - - flags.VarP(&opts.labels, flagLabel, "l", "Service labels") - flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") - flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") - flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") - flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") - flags.Var(&opts.constraints, flagConstraint, "Placement constraints") - flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference") - flags.SetAnnotation(flagPlacementPref, "version", []string{"1.28"}) - flags.Var(&opts.networks, flagNetwork, "Network attachments") - flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") - flags.SetAnnotation(flagSecret, "version", []string{"1.25"}) - flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") - flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") - flags.SetAnnotation(flagGroup, "version", []string{"1.25"}) - flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") - flags.SetAnnotation(flagDNS, "version", []string{"1.25"}) - flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") - flags.SetAnnotation(flagDNSOption, "version", []string{"1.25"}) - flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") - flags.SetAnnotation(flagDNSSearch, "version", []string{"1.25"}) - flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") - flags.SetAnnotation(flagHost, "version", []string{"1.25"}) - - flags.SetInterspersed(false) - return cmd -} - -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error { - apiClient := dockerCli.Client() - createOpts := types.ServiceCreateOptions{} - - ctx := context.Background() - - service, err := opts.ToService(ctx, apiClient, flags) - if err != nil { - return err - } - - specifiedSecrets := opts.secrets.Value() - if len(specifiedSecrets) > 0 { - // parse and validate secrets - secrets, err := ParseSecrets(apiClient, specifiedSecrets) - if err != nil { - return err - } - service.TaskTemplate.ContainerSpec.Secrets = secrets - - } - - if err := resolveServiceImageDigest(dockerCli, &service); err != nil { - return err - } - - // only send auth if flag was set - if opts.registryAuth { - // Retrieve encoded auth token from the image reference - encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, opts.image) - if err != nil { - return err - } - createOpts.EncodedRegistryAuth = encodedAuth - } - - response, err := apiClient.ServiceCreate(ctx, service, createOpts) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) - - if opts.detach { - if !flags.Changed("detach") { - fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ - "In a future release, --detach=false will become the default.") - } - return nil - } - - return waitOnService(ctx, dockerCli, response.ID, opts) -} diff --git a/cli/command/service/helpers.go b/cli/command/service/helpers.go deleted file mode 100644 index 2289369908..0000000000 --- a/cli/command/service/helpers.go +++ /dev/null @@ -1,39 +0,0 @@ -package service - -import ( - "io" - - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/service/progress" - "github.com/docker/docker/pkg/jsonmessage" - "golang.org/x/net/context" -) - -// waitOnService waits for the service to converge. It outputs a progress bar, -// if appopriate based on the CLI flags. -func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, opts *serviceOptions) error { - errChan := make(chan error, 1) - pipeReader, pipeWriter := io.Pipe() - - go func() { - errChan <- progress.ServiceProgress(ctx, dockerCli.Client(), serviceID, pipeWriter) - }() - - if opts.quiet { - go func() { - for { - var buf [1024]byte - if _, err := pipeReader.Read(buf[:]); err != nil { - return - } - } - }() - return <-errChan - } - - err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) - if err == nil { - err = <-errChan - } - return err -} diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go deleted file mode 100644 index fae24eeaf1..0000000000 --- a/cli/command/service/inspect.go +++ /dev/null @@ -1,94 +0,0 @@ -package service - -import ( - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - apiclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - refs []string - format string - pretty bool -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] SERVICE [SERVICE...]", - Short: "Display detailed information on one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - - if opts.pretty && len(opts.format) > 0 { - return errors.Errorf("--format is incompatible with human friendly format") - } - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.pretty { - opts.format = "pretty" - } - - getRef := func(ref string) (interface{}, []byte, error) { - // Service inspect shows defaults values in empty fields. - service, _, err := client.ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true}) - if err == nil || !apiclient.IsErrServiceNotFound(err) { - return service, nil, err - } - return nil, nil, errors.Errorf("Error: no such service: %s", ref) - } - - getNetwork := func(ref string) (interface{}, []byte, error) { - network, _, err := client.NetworkInspectWithRaw(ctx, ref, false) - if err == nil || !apiclient.IsErrNetworkNotFound(err) { - return network, nil, err - } - return nil, nil, errors.Errorf("Error: no such network: %s", ref) - } - - f := opts.format - if len(f) == 0 { - f = "raw" - if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { - f = dockerCli.ConfigFile().ServiceInspectFormat - } - } - - // check if the user is trying to apply a template to the pretty format, which - // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { - return errors.Errorf("Cannot supply extra formatting options to the pretty template") - } - - serviceCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceFormat(f), - } - - if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { - return cli.StatusError{StatusCode: 1, Status: err.Error()} - } - return nil -} diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go deleted file mode 100644 index c5bda7dcd9..0000000000 --- a/cli/command/service/inspect_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command/formatter" - "github.com/stretchr/testify/assert" -) - -func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string { - b := new(bytes.Buffer) - - endpointSpec := &swarm.EndpointSpec{ - Mode: "vip", - Ports: []swarm.PortConfig{ - { - Protocol: swarm.PortConfigProtocolTCP, - TargetPort: 5000, - }, - }, - } - - two := uint64(2) - - s := swarm.Service{ - ID: "de179gar9d0o7ltdybungplod", - Meta: swarm.Meta{ - Version: swarm.Version{Index: 315}, - CreatedAt: now, - UpdatedAt: now, - }, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: "my_service", - Labels: map[string]string{"com.label": "foo"}, - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: "foo/bar@sha256:this_is_a_test", - }, - Networks: []swarm.NetworkAttachmentConfig{ - { - Target: "5vpyomhb6ievnk0i0o60gcnei", - Aliases: []string{"web"}, - }, - }, - }, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{ - Replicas: &two, - }, - }, - EndpointSpec: endpointSpec, - }, - Endpoint: swarm.Endpoint{ - Spec: *endpointSpec, - Ports: []swarm.PortConfig{ - { - Protocol: swarm.PortConfigProtocolTCP, - TargetPort: 5000, - PublishedPort: 30000, - }, - }, - VirtualIPs: []swarm.EndpointVirtualIP{ - { - NetworkID: "6o4107cj2jx9tihgb0jyts6pj", - Addr: "10.255.0.4/16", - }, - }, - }, - UpdateStatus: &swarm.UpdateStatus{ - StartedAt: &now, - CompletedAt: &now, - }, - } - - ctx := formatter.Context{ - Output: b, - Format: format, - } - - err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, - func(ref string) (interface{}, []byte, error) { - return s, nil, nil - }, - func(ref string) (interface{}, []byte, error) { - return types.NetworkResource{ - ID: "5vpyomhb6ievnk0i0o60gcnei", - Name: "mynetwork", - }, nil, nil - }, - ) - if err != nil { - t.Fatal(err) - } - return b.String() -} - -func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { - s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now()) - if strings.Contains(s, "UpdateStatus") { - t.Fatal("Pretty print failed before parsing UpdateStatus") - } - if !strings.Contains(s, "mynetwork") { - t.Fatal("network name not found in inspect output") - } -} - -func TestJSONFormatWithNoUpdateConfig(t *testing.T) { - now := time.Now() - // s1: [{"ID":..}] - // s2: {"ID":..} - s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now) - t.Log("// s1") - t.Logf("%s", s1) - s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now) - t.Log("// s2") - t.Logf("%s", s2) - var m1Wrap []map[string]interface{} - if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { - t.Fatal(err) - } - if len(m1Wrap) != 1 { - t.Fatalf("strange s1=%s", s1) - } - m1 := m1Wrap[0] - t.Logf("m1=%+v", m1) - var m2 map[string]interface{} - if err := json.Unmarshal([]byte(s2), &m2); err != nil { - t.Fatal(err) - } - t.Logf("m2=%+v", m2) - assert.Equal(t, m1, m2) -} diff --git a/cli/command/service/list.go b/cli/command/service/list.go deleted file mode 100644 index 9c4987806f..0000000000 --- a/cli/command/service/list.go +++ /dev/null @@ -1,129 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List services", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - serviceFilters := opts.filter.Value() - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters}) - if err != nil { - return err - } - - info := map[string]formatter.ServiceListInfo{} - if len(services) > 0 && !opts.quiet { - // only non-empty services and not quiet, should we call TaskList and NodeList api - taskFilter := filters.NewArgs() - for _, service := range services { - taskFilter.Add("service", service.ID) - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - if err != nil { - return err - } - - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return err - } - - info = GetServicesStatus(services, nodes, tasks) - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ServicesFormat - } else { - format = formatter.TableFormatKey - } - } - - servicesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, opts.quiet), - } - return formatter.ServiceListWrite(servicesCtx, services, info) -} - -// GetServicesStatus returns a map of mode and replicas -func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo { - running := map[string]int{} - tasksNoShutdown := map[string]int{} - - activeNodes := make(map[string]struct{}) - for _, n := range nodes { - if n.Status.State != swarm.NodeStateDown { - activeNodes[n.ID] = struct{}{} - } - } - - for _, task := range tasks { - if task.DesiredState != swarm.TaskStateShutdown { - tasksNoShutdown[task.ServiceID]++ - } - - if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning { - running[task.ServiceID]++ - } - } - - info := map[string]formatter.ServiceListInfo{} - for _, service := range services { - info[service.ID] = formatter.ServiceListInfo{} - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - info[service.ID] = formatter.ServiceListInfo{ - Mode: "replicated", - Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), - } - } else if service.Spec.Mode.Global != nil { - info[service.ID] = formatter.ServiceListInfo{ - Mode: "global", - Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]), - } - } - } - return info -} diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go deleted file mode 100644 index 32a3f3557a..0000000000 --- a/cli/command/service/logs.go +++ /dev/null @@ -1,301 +0,0 @@ -package service - -import ( - "bytes" - "fmt" - "io" - "strconv" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/docker/pkg/stringid" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type logsOptions struct { - noResolve bool - noTrunc bool - noTaskIDs bool - follow bool - since string - timestamps bool - tail string - - target string -} - -// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs -// to be refactored agressively. There may be changes to the implementation of -// details, which will be need to be reflected in this code. The refactoring -// should be put off until we make those changes, tho, because I think the -// decisions made WRT details will impact the design of the CLI. -func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts logsOptions - - cmd := &cobra.Command{ - Use: "logs [OPTIONS] SERVICE|TASK", - Short: "Fetch the logs of a service or task", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.target = args[0] - return runLogs(dockerCli, &opts) - }, - Tags: map[string]string{"version": "1.29"}, - } - - flags := cmd.Flags() - // options specific to service logs - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") - // options identical to container logs - flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") - flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") - flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") - return cmd -} - -func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { - ctx := context.Background() - - options := types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: opts.since, - Timestamps: opts.timestamps, - Follow: opts.follow, - Tail: opts.tail, - Details: true, - } - - cli := dockerCli.Client() - - var ( - maxLength = 1 - responseBody io.ReadCloser - tty bool - ) - - service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{}) - if err != nil { - // if it's any error other than service not found, it's Real - if !client.IsErrServiceNotFound(err) { - return err - } - task, _, err := cli.TaskInspectWithRaw(ctx, opts.target) - if err != nil { - if client.IsErrTaskNotFound(err) { - // if the task isn't found, rewrite the error to be clear - // that we looked for services AND tasks and found none - err = fmt.Errorf("no such task or service") - } - return err - } - tty = task.Spec.ContainerSpec.TTY - // TODO(dperny) hot fix until we get a nice details system squared away, - // ignores details (including task context) if we have a TTY log - // if we don't do this, we'll vomit the huge context verbatim into the - // TTY log lines and that's Undesirable. - if tty { - options.Details = false - } - - responseBody, err = cli.TaskLogs(ctx, opts.target, options) - if err != nil { - return err - } - - maxLength = getMaxLength(task.Slot) - } else { - tty = service.Spec.TaskTemplate.ContainerSpec.TTY - // TODO(dperny) hot fix until we get a nice details system squared away, - // ignores details (including task context) if we have a TTY log - if tty { - options.Details = false - } - - responseBody, err = cli.ServiceLogs(ctx, opts.target, options) - if err != nil { - return err - } - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - // if replicas are initialized, figure out if we need to pad them - replicas := *service.Spec.Mode.Replicated.Replicas - maxLength = getMaxLength(int(replicas)) - } - } - defer responseBody.Close() - - if tty { - _, err = io.Copy(dockerCli.Out(), responseBody) - return err - } - - taskFormatter := newTaskFormatter(cli, opts, maxLength) - - stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()} - stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()} - - // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. - _, err = stdcopy.StdCopy(stdout, stderr, responseBody) - return err -} - -// getMaxLength gets the maximum length of the number in base 10 -func getMaxLength(i int) int { - return len(strconv.FormatInt(int64(i), 10)) -} - -type taskFormatter struct { - client client.APIClient - opts *logsOptions - padding int - - r *idresolver.IDResolver - cache map[logContext]string -} - -func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter { - return &taskFormatter{ - client: client, - opts: opts, - padding: padding, - r: idresolver.New(client, opts.noResolve), - cache: make(map[logContext]string), - } -} - -func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) { - if cached, ok := f.cache[logCtx]; ok { - return cached, nil - } - - nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID) - if err != nil { - return "", err - } - - serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID) - if err != nil { - return "", err - } - - task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID) - if err != nil { - return "", err - } - - taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) - if !f.opts.noTaskIDs { - if f.opts.noTrunc { - taskName += fmt.Sprintf(".%s", task.ID) - } else { - taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) - } - } - - padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot)) - formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding) - f.cache[logCtx] = formatted - return formatted, nil -} - -type logWriter struct { - ctx context.Context - opts *logsOptions - f *taskFormatter - w io.Writer -} - -func (lw *logWriter) Write(buf []byte) (int, error) { - contextIndex := 0 - numParts := 2 - if lw.opts.timestamps { - contextIndex++ - numParts++ - } - - parts := bytes.SplitN(buf, []byte(" "), numParts) - if len(parts) != numParts { - return 0, errors.Errorf("invalid context in log message: %v", string(buf)) - } - - logCtx, err := lw.parseContext(string(parts[contextIndex])) - if err != nil { - return 0, err - } - - output := []byte{} - for i, part := range parts { - // First part doesn't get space separation. - if i > 0 { - output = append(output, []byte(" ")...) - } - - if i == contextIndex { - formatted, err := lw.f.format(lw.ctx, logCtx) - if err != nil { - return 0, err - } - output = append(output, []byte(fmt.Sprintf("%s |", formatted))...) - } else { - output = append(output, part...) - } - } - _, err = lw.w.Write(output) - if err != nil { - return 0, err - } - - return len(buf), nil -} - -func (lw *logWriter) parseContext(input string) (logContext, error) { - context := make(map[string]string) - - components := strings.Split(input, ",") - for _, component := range components { - parts := strings.SplitN(component, "=", 2) - if len(parts) != 2 { - return logContext{}, errors.Errorf("invalid context: %s", input) - } - context[parts[0]] = parts[1] - } - - nodeID, ok := context["com.docker.swarm.node.id"] - if !ok { - return logContext{}, errors.Errorf("missing node id in context: %s", input) - } - - serviceID, ok := context["com.docker.swarm.service.id"] - if !ok { - return logContext{}, errors.Errorf("missing service id in context: %s", input) - } - - taskID, ok := context["com.docker.swarm.task.id"] - if !ok { - return logContext{}, errors.Errorf("missing task id in context: %s", input) - } - - return logContext{ - nodeID: nodeID, - serviceID: serviceID, - taskID: taskID, - }, nil -} - -type logContext struct { - nodeID string - serviceID string - taskID string -} diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go deleted file mode 100644 index ecc37d7b00..0000000000 --- a/cli/command/service/opts.go +++ /dev/null @@ -1,912 +0,0 @@ -package service - -import ( - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/swarmkit/api" - "github.com/docker/swarmkit/api/defaults" - shlex "github.com/flynn-archive/go-shlex" - gogotypes "github.com/gogo/protobuf/types" - "github.com/pkg/errors" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type int64Value interface { - Value() int64 -} - -// PositiveDurationOpt is an option type for time.Duration that uses a pointer. -// It bahave similarly to DurationOpt but only allows positive duration values. -type PositiveDurationOpt struct { - DurationOpt -} - -// Set a new value on the option. Setting a negative duration value will cause -// an error to be returned. -func (d *PositiveDurationOpt) Set(s string) error { - err := d.DurationOpt.Set(s) - if err != nil { - return err - } - if *d.DurationOpt.value < 0 { - return errors.Errorf("duration cannot be negative") - } - return nil -} - -// DurationOpt is an option type for time.Duration that uses a pointer. This -// allows us to get nil values outside, instead of defaulting to 0 -type DurationOpt struct { - value *time.Duration -} - -// Set a new value on the option -func (d *DurationOpt) Set(s string) error { - v, err := time.ParseDuration(s) - d.value = &v - return err -} - -// Type returns the type of this option, which will be displayed in `--help` output -func (d *DurationOpt) Type() string { - return "duration" -} - -// String returns a string repr of this option -func (d *DurationOpt) String() string { - if d.value != nil { - return d.value.String() - } - return "" -} - -// Value returns the time.Duration -func (d *DurationOpt) Value() *time.Duration { - return d.value -} - -// Uint64Opt represents a uint64. -type Uint64Opt struct { - value *uint64 -} - -// Set a new value on the option -func (i *Uint64Opt) Set(s string) error { - v, err := strconv.ParseUint(s, 0, 64) - i.value = &v - return err -} - -// Type returns the type of this option, which will be displayed in `--help` output -func (i *Uint64Opt) Type() string { - return "uint" -} - -// String returns a string repr of this option -func (i *Uint64Opt) String() string { - if i.value != nil { - return fmt.Sprintf("%v", *i.value) - } - return "" -} - -// Value returns the uint64 -func (i *Uint64Opt) Value() *uint64 { - return i.value -} - -type floatValue float32 - -func (f *floatValue) Set(s string) error { - v, err := strconv.ParseFloat(s, 32) - *f = floatValue(v) - return err -} - -func (f *floatValue) Type() string { - return "float" -} - -func (f *floatValue) String() string { - return strconv.FormatFloat(float64(*f), 'g', -1, 32) -} - -func (f *floatValue) Value() float32 { - return float32(*f) -} - -// placementPrefOpts holds a list of placement preferences. -type placementPrefOpts struct { - prefs []swarm.PlacementPreference - strings []string -} - -func (opts *placementPrefOpts) String() string { - if len(opts.strings) == 0 { - return "" - } - return fmt.Sprintf("%v", opts.strings) -} - -// Set validates the input value and adds it to the internal slices. -// Note: in the future strategies other than "spread", may be supported, -// as well as additional comma-separated options. -func (opts *placementPrefOpts) Set(value string) error { - fields := strings.Split(value, "=") - if len(fields) != 2 { - return errors.New(`placement preference must be of the format "="`) - } - if fields[0] != "spread" { - return errors.Errorf("unsupported placement preference %s (only spread is supported)", fields[0]) - } - - opts.prefs = append(opts.prefs, swarm.PlacementPreference{ - Spread: &swarm.SpreadOver{ - SpreadDescriptor: fields[1], - }, - }) - opts.strings = append(opts.strings, value) - return nil -} - -// Type returns a string name for this Option type -func (opts *placementPrefOpts) Type() string { - return "pref" -} - -// ShlexOpt is a flag Value which parses a string as a list of shell words -type ShlexOpt []string - -// Set the value -func (s *ShlexOpt) Set(value string) error { - valueSlice, err := shlex.Split(value) - *s = ShlexOpt(valueSlice) - return err -} - -// Type returns the tyep of the value -func (s *ShlexOpt) Type() string { - return "command" -} - -func (s *ShlexOpt) String() string { - if len(*s) == 0 { - return "" - } - return fmt.Sprint(*s) -} - -// Value returns the value as a string slice -func (s *ShlexOpt) Value() []string { - return []string(*s) -} - -type updateOptions struct { - parallelism uint64 - delay time.Duration - monitor time.Duration - onFailure string - maxFailureRatio floatValue - order string -} - -func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.UpdateConfig { - defaultFailureAction := strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaultUpdateConfig.FailureAction)]) - defaultMonitor, _ := gogotypes.DurationFromProto(defaultUpdateConfig.Monitor) - return &swarm.UpdateConfig{ - Parallelism: defaultUpdateConfig.Parallelism, - Delay: defaultUpdateConfig.Delay, - Monitor: defaultMonitor, - FailureAction: defaultFailureAction, - MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio, - Order: defaultOrder(defaultUpdateConfig.Order), - } -} - -func (opts updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { - if !anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio) { - return nil - } - - updateConfig := updateConfigFromDefaults(defaults.Service.Update) - - if flags.Changed(flagUpdateParallelism) { - updateConfig.Parallelism = opts.parallelism - } - if flags.Changed(flagUpdateDelay) { - updateConfig.Delay = opts.delay - } - if flags.Changed(flagUpdateMonitor) { - updateConfig.Monitor = opts.monitor - } - if flags.Changed(flagUpdateFailureAction) { - updateConfig.FailureAction = opts.onFailure - } - if flags.Changed(flagUpdateMaxFailureRatio) { - updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value() - } - if flags.Changed(flagUpdateOrder) { - updateConfig.Order = opts.order - } - - return updateConfig -} - -func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { - if !anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio) { - return nil - } - - updateConfig := updateConfigFromDefaults(defaults.Service.Rollback) - - if flags.Changed(flagRollbackParallelism) { - updateConfig.Parallelism = opts.parallelism - } - if flags.Changed(flagRollbackDelay) { - updateConfig.Delay = opts.delay - } - if flags.Changed(flagRollbackMonitor) { - updateConfig.Monitor = opts.monitor - } - if flags.Changed(flagRollbackFailureAction) { - updateConfig.FailureAction = opts.onFailure - } - if flags.Changed(flagRollbackMaxFailureRatio) { - updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value() - } - if flags.Changed(flagRollbackOrder) { - updateConfig.Order = opts.order - } - - return updateConfig -} - -type resourceOptions struct { - limitCPU opts.NanoCPUs - limitMemBytes opts.MemBytes - resCPU opts.NanoCPUs - resMemBytes opts.MemBytes -} - -func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements { - return &swarm.ResourceRequirements{ - Limits: &swarm.Resources{ - NanoCPUs: r.limitCPU.Value(), - MemoryBytes: r.limitMemBytes.Value(), - }, - Reservations: &swarm.Resources{ - NanoCPUs: r.resCPU.Value(), - MemoryBytes: r.resMemBytes.Value(), - }, - } -} - -type restartPolicyOptions struct { - condition string - delay DurationOpt - maxAttempts Uint64Opt - window DurationOpt -} - -func defaultRestartPolicy() *swarm.RestartPolicy { - defaultMaxAttempts := defaults.Service.Task.Restart.MaxAttempts - rp := &swarm.RestartPolicy{ - MaxAttempts: &defaultMaxAttempts, - } - - if defaults.Service.Task.Restart.Delay != nil { - defaultRestartDelay, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) - rp.Delay = &defaultRestartDelay - } - if defaults.Service.Task.Restart.Window != nil { - defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) - rp.Window = &defaultRestartWindow - } - rp.Condition = defaultRestartCondition() - - return rp -} - -func defaultRestartCondition() swarm.RestartPolicyCondition { - switch defaults.Service.Task.Restart.Condition { - case api.RestartOnNone: - return "none" - case api.RestartOnFailure: - return "on-failure" - case api.RestartOnAny: - return "any" - default: - return "" - } -} - -func defaultOrder(order api.UpdateConfig_UpdateOrder) string { - switch order { - case api.UpdateConfig_STOP_FIRST: - return "stop-first" - case api.UpdateConfig_START_FIRST: - return "start-first" - default: - return "" - } -} - -func (r *restartPolicyOptions) ToRestartPolicy(flags *pflag.FlagSet) *swarm.RestartPolicy { - if !anyChanged(flags, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow, flagRestartCondition) { - return nil - } - - restartPolicy := defaultRestartPolicy() - - if flags.Changed(flagRestartDelay) { - restartPolicy.Delay = r.delay.Value() - } - if flags.Changed(flagRestartCondition) { - restartPolicy.Condition = swarm.RestartPolicyCondition(r.condition) - } - if flags.Changed(flagRestartMaxAttempts) { - restartPolicy.MaxAttempts = r.maxAttempts.Value() - } - if flags.Changed(flagRestartWindow) { - restartPolicy.Window = r.window.Value() - } - - return restartPolicy -} - -type credentialSpecOpt struct { - value *swarm.CredentialSpec - source string -} - -func (c *credentialSpecOpt) Set(value string) error { - c.source = value - c.value = &swarm.CredentialSpec{} - switch { - case strings.HasPrefix(value, "file://"): - c.value.File = strings.TrimPrefix(value, "file://") - case strings.HasPrefix(value, "registry://"): - c.value.Registry = strings.TrimPrefix(value, "registry://") - default: - return errors.New("Invalid credential spec - value must be prefixed file:// or registry:// followed by a value") - } - - return nil -} - -func (c *credentialSpecOpt) Type() string { - return "credential-spec" -} - -func (c *credentialSpecOpt) String() string { - return c.source -} - -func (c *credentialSpecOpt) Value() *swarm.CredentialSpec { - return c.value -} - -func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks []string) ([]swarm.NetworkAttachmentConfig, error) { - nets := []swarm.NetworkAttachmentConfig{} - for _, networkIDOrName := range networks { - network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false) - if err != nil { - return nil, err - } - nets = append(nets, swarm.NetworkAttachmentConfig{Target: network.ID}) - } - sort.Sort(byNetworkTarget(nets)) - return nets, nil -} - -type endpointOptions struct { - mode string - publishPorts opts.PortOpt -} - -func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { - return &swarm.EndpointSpec{ - Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), - Ports: e.publishPorts.Value(), - } -} - -type logDriverOptions struct { - name string - opts opts.ListOpts -} - -func newLogDriverOptions() logDriverOptions { - return logDriverOptions{opts: opts.NewListOpts(opts.ValidateEnv)} -} - -func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { - if ldo.name == "" { - return nil - } - - // set the log driver only if specified. - return &swarm.Driver{ - Name: ldo.name, - Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()), - } -} - -type healthCheckOptions struct { - cmd string - interval PositiveDurationOpt - timeout PositiveDurationOpt - retries int - startPeriod PositiveDurationOpt - noHealthcheck bool -} - -func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { - var healthConfig *container.HealthConfig - haveHealthSettings := opts.cmd != "" || - opts.interval.Value() != nil || - opts.timeout.Value() != nil || - opts.retries != 0 - if opts.noHealthcheck { - if haveHealthSettings { - return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) - } - healthConfig = &container.HealthConfig{Test: []string{"NONE"}} - } else if haveHealthSettings { - var test []string - if opts.cmd != "" { - test = []string{"CMD-SHELL", opts.cmd} - } - var interval, timeout, startPeriod time.Duration - if ptr := opts.interval.Value(); ptr != nil { - interval = *ptr - } - if ptr := opts.timeout.Value(); ptr != nil { - timeout = *ptr - } - if ptr := opts.startPeriod.Value(); ptr != nil { - startPeriod = *ptr - } - healthConfig = &container.HealthConfig{ - Test: test, - Interval: interval, - Timeout: timeout, - Retries: opts.retries, - StartPeriod: startPeriod, - } - } - return healthConfig, nil -} - -// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli -// : -// into a swarmkit host format: -// IP_address canonical_hostname [aliases...] -// This assumes input value (:) has already been validated -func convertExtraHostsToSwarmHosts(extraHosts []string) []string { - hosts := []string{} - for _, extraHost := range extraHosts { - parts := strings.SplitN(extraHost, ":", 2) - hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) - } - return hosts -} - -type serviceOptions struct { - detach bool - quiet bool - - name string - labels opts.ListOpts - containerLabels opts.ListOpts - image string - entrypoint ShlexOpt - args []string - hostname string - env opts.ListOpts - envFile opts.ListOpts - workdir string - user string - groups opts.ListOpts - credentialSpec credentialSpecOpt - stopSignal string - tty bool - readOnly bool - mounts opts.MountOpt - dns opts.ListOpts - dnsSearch opts.ListOpts - dnsOption opts.ListOpts - hosts opts.ListOpts - - resources resourceOptions - stopGrace DurationOpt - - replicas Uint64Opt - mode string - - restartPolicy restartPolicyOptions - constraints opts.ListOpts - placementPrefs placementPrefOpts - update updateOptions - rollback updateOptions - networks opts.ListOpts - endpoint endpointOptions - - registryAuth bool - - logDriver logDriverOptions - - healthcheck healthCheckOptions - secrets opts.SecretOpt -} - -func newServiceOptions() *serviceOptions { - return &serviceOptions{ - labels: opts.NewListOpts(opts.ValidateEnv), - constraints: opts.NewListOpts(nil), - containerLabels: opts.NewListOpts(opts.ValidateEnv), - env: opts.NewListOpts(opts.ValidateEnv), - envFile: opts.NewListOpts(nil), - groups: opts.NewListOpts(nil), - logDriver: newLogDriverOptions(), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOption: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - hosts: opts.NewListOpts(opts.ValidateExtraHost), - networks: opts.NewListOpts(nil), - } -} - -func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { - serviceMode := swarm.ServiceMode{} - switch opts.mode { - case "global": - if opts.replicas.Value() != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated mode") - } - - serviceMode.Global = &swarm.GlobalService{} - case "replicated": - serviceMode.Replicated = &swarm.ReplicatedService{ - Replicas: opts.replicas.Value(), - } - default: - return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode) - } - return serviceMode, nil -} - -func (opts *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration { - if flags.Changed(flagStopGracePeriod) { - return opts.stopGrace.Value() - } - return nil -} - -func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) { - var service swarm.ServiceSpec - - envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) - if err != nil { - return service, err - } - - currentEnv := make([]string, 0, len(envVariables)) - for _, env := range envVariables { // need to process each var, in order - k := strings.SplitN(env, "=", 2)[0] - for i, current := range currentEnv { // remove duplicates - if current == env { - continue // no update required, may hide this behind flag to preserve order of envVariables - } - if strings.HasPrefix(current, k+"=") { - currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) - } - } - currentEnv = append(currentEnv, env) - } - - healthConfig, err := opts.healthcheck.toHealthConfig() - if err != nil { - return service, err - } - - serviceMode, err := opts.ToServiceMode() - if err != nil { - return service, err - } - - networks, err := convertNetworks(ctx, apiClient, opts.networks.GetAll()) - if err != nil { - return service, err - } - - service = swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: opts.name, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: opts.image, - Args: opts.args, - Command: opts.entrypoint.Value(), - Env: currentEnv, - Hostname: opts.hostname, - Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), - Dir: opts.workdir, - User: opts.user, - Groups: opts.groups.GetAll(), - StopSignal: opts.stopSignal, - TTY: opts.tty, - ReadOnly: opts.readOnly, - Mounts: opts.mounts.Value(), - DNSConfig: &swarm.DNSConfig{ - Nameservers: opts.dns.GetAll(), - Search: opts.dnsSearch.GetAll(), - Options: opts.dnsOption.GetAll(), - }, - Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), - StopGracePeriod: opts.ToStopGracePeriod(flags), - Secrets: nil, - Healthcheck: healthConfig, - }, - Networks: networks, - Resources: opts.resources.ToResourceRequirements(), - RestartPolicy: opts.restartPolicy.ToRestartPolicy(flags), - Placement: &swarm.Placement{ - Constraints: opts.constraints.GetAll(), - Preferences: opts.placementPrefs.prefs, - }, - LogDriver: opts.logDriver.toLogDriver(), - }, - Mode: serviceMode, - UpdateConfig: opts.update.updateConfig(flags), - RollbackConfig: opts.update.rollbackConfig(flags), - EndpointSpec: opts.endpoint.ToEndpointSpec(), - } - - if opts.credentialSpec.Value() != nil { - service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{ - CredentialSpec: opts.credentialSpec.Value(), - } - } - - return service, nil -} - -type flagDefaults map[string]interface{} - -func (fd flagDefaults) getUint64(flagName string) uint64 { - if val, ok := fd[flagName].(uint64); ok { - return val - } - return 0 -} - -func (fd flagDefaults) getString(flagName string) string { - if val, ok := fd[flagName].(string); ok { - return val - } - return "" -} - -func buildServiceDefaultFlagMapping() flagDefaults { - defaultFlagValues := make(map[string]interface{}) - - defaultFlagValues[flagStopGracePeriod], _ = gogotypes.DurationFromProto(defaults.Service.Task.GetContainer().StopGracePeriod) - defaultFlagValues[flagRestartCondition] = `"` + defaultRestartCondition() + `"` - defaultFlagValues[flagRestartDelay], _ = gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) - - if defaults.Service.Task.Restart.MaxAttempts != 0 { - defaultFlagValues[flagRestartMaxAttempts] = defaults.Service.Task.Restart.MaxAttempts - } - - defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) - if defaultRestartWindow != 0 { - defaultFlagValues[flagRestartWindow] = defaultRestartWindow - } - - defaultFlagValues[flagUpdateParallelism] = defaults.Service.Update.Parallelism - defaultFlagValues[flagUpdateDelay] = defaults.Service.Update.Delay - defaultFlagValues[flagUpdateMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Update.Monitor) - defaultFlagValues[flagUpdateFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Update.FailureAction)]) + `"` - defaultFlagValues[flagUpdateMaxFailureRatio] = defaults.Service.Update.MaxFailureRatio - defaultFlagValues[flagUpdateOrder] = `"` + defaultOrder(defaults.Service.Update.Order) + `"` - - defaultFlagValues[flagRollbackParallelism] = defaults.Service.Rollback.Parallelism - defaultFlagValues[flagRollbackDelay] = defaults.Service.Rollback.Delay - defaultFlagValues[flagRollbackMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Rollback.Monitor) - defaultFlagValues[flagRollbackFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Rollback.FailureAction)]) + `"` - defaultFlagValues[flagRollbackMaxFailureRatio] = defaults.Service.Rollback.MaxFailureRatio - defaultFlagValues[flagRollbackOrder] = `"` + defaultOrder(defaults.Service.Rollback.Order) + `"` - - defaultFlagValues[flagEndpointMode] = "vip" - - return defaultFlagValues -} - -// addServiceFlags adds all flags that are common to both `create` and `update`. -// Any flags that are not common are added separately in the individual command -func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValues flagDefaults) { - flagDesc := func(flagName string, desc string) string { - if defaultValue, ok := defaultFlagValues[flagName]; ok { - return fmt.Sprintf("%s (default %v)", desc, defaultValue) - } - return desc - } - - flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the service to converge") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output") - - flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") - flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") - flags.Var(&opts.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)") - flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"}) - flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") - flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) - flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") - - flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") - flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") - flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") - flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") - - flags.Var(&opts.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) - flags.Var(&opts.replicas, flagReplicas, "Number of tasks") - - flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none"|"on-failure"|"any")`)) - flags.Var(&opts.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)")) - flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, flagDesc(flagRestartMaxAttempts, "Maximum number of restarts before giving up")) - - flags.Var(&opts.restartPolicy.window, flagRestartWindow, flagDesc(flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)")) - - flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, defaultFlagValues.getUint64(flagUpdateParallelism), "Maximum number of tasks updated simultaneously (0 to update all at once)") - flags.DurationVar(&opts.update.delay, flagUpdateDelay, 0, flagDesc(flagUpdateDelay, "Delay between updates (ns|us|ms|s|m|h)")) - flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, 0, flagDesc(flagUpdateMonitor, "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) - flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "", flagDesc(flagUpdateFailureAction, `Action on update failure ("pause"|"continue"|"rollback")`)) - flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, flagDesc(flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update")) - flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) - flags.StringVar(&opts.update.order, flagUpdateOrder, "", flagDesc(flagUpdateOrder, `Update order ("start-first"|"stop-first")`)) - flags.SetAnnotation(flagUpdateOrder, "version", []string{"1.29"}) - - flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, defaultFlagValues.getUint64(flagRollbackParallelism), "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)") - flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"}) - flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, 0, flagDesc(flagRollbackDelay, "Delay between task rollbacks (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"}) - flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, 0, flagDesc(flagRollbackMonitor, "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"}) - flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "", flagDesc(flagRollbackFailureAction, `Action on rollback failure ("pause"|"continue")`)) - flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"}) - flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, flagDesc(flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback")) - flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"}) - flags.StringVar(&opts.rollback.order, flagRollbackOrder, "", flagDesc(flagRollbackOrder, `Rollback order ("start-first"|"stop-first")`)) - flags.SetAnnotation(flagRollbackOrder, "version", []string{"1.29"}) - - flags.StringVar(&opts.endpoint.mode, flagEndpointMode, defaultFlagValues.getString(flagEndpointMode), "Endpoint mode (vip or dnsrr)") - - flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") - - flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") - flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") - - flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") - flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ms|s|m|h)") - flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ms|s|m|h)") - flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) - flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") - flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)") - flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"}) - flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") - flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) - - flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") - flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) - - flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") - flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"}) - - flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container") - flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"}) -} - -const ( - flagCredentialSpec = "credential-spec" - flagPlacementPref = "placement-pref" - flagPlacementPrefAdd = "placement-pref-add" - flagPlacementPrefRemove = "placement-pref-rm" - flagConstraint = "constraint" - flagConstraintRemove = "constraint-rm" - flagConstraintAdd = "constraint-add" - flagContainerLabel = "container-label" - flagContainerLabelRemove = "container-label-rm" - flagContainerLabelAdd = "container-label-add" - flagDNS = "dns" - flagDNSRemove = "dns-rm" - flagDNSAdd = "dns-add" - flagDNSOption = "dns-option" - flagDNSOptionRemove = "dns-option-rm" - flagDNSOptionAdd = "dns-option-add" - flagDNSSearch = "dns-search" - flagDNSSearchRemove = "dns-search-rm" - flagDNSSearchAdd = "dns-search-add" - flagEndpointMode = "endpoint-mode" - flagEntrypoint = "entrypoint" - flagHost = "host" - flagHostAdd = "host-add" - flagHostRemove = "host-rm" - flagHostname = "hostname" - flagEnv = "env" - flagEnvFile = "env-file" - flagEnvRemove = "env-rm" - flagEnvAdd = "env-add" - flagGroup = "group" - flagGroupAdd = "group-add" - flagGroupRemove = "group-rm" - flagLabel = "label" - flagLabelRemove = "label-rm" - flagLabelAdd = "label-add" - flagLimitCPU = "limit-cpu" - flagLimitMemory = "limit-memory" - flagMode = "mode" - flagMount = "mount" - flagMountRemove = "mount-rm" - flagMountAdd = "mount-add" - flagName = "name" - flagNetwork = "network" - flagNetworkAdd = "network-add" - flagNetworkRemove = "network-rm" - flagPublish = "publish" - flagPublishRemove = "publish-rm" - flagPublishAdd = "publish-add" - flagReadOnly = "read-only" - flagReplicas = "replicas" - flagReserveCPU = "reserve-cpu" - flagReserveMemory = "reserve-memory" - flagRestartCondition = "restart-condition" - flagRestartDelay = "restart-delay" - flagRestartMaxAttempts = "restart-max-attempts" - flagRestartWindow = "restart-window" - flagRollbackDelay = "rollback-delay" - flagRollbackFailureAction = "rollback-failure-action" - flagRollbackMaxFailureRatio = "rollback-max-failure-ratio" - flagRollbackMonitor = "rollback-monitor" - flagRollbackOrder = "rollback-order" - flagRollbackParallelism = "rollback-parallelism" - flagStopGracePeriod = "stop-grace-period" - flagStopSignal = "stop-signal" - flagTTY = "tty" - flagUpdateDelay = "update-delay" - flagUpdateFailureAction = "update-failure-action" - flagUpdateMaxFailureRatio = "update-max-failure-ratio" - flagUpdateMonitor = "update-monitor" - flagUpdateOrder = "update-order" - flagUpdateParallelism = "update-parallelism" - flagUser = "user" - flagWorkdir = "workdir" - flagRegistryAuth = "with-registry-auth" - flagLogDriver = "log-driver" - flagLogOpt = "log-opt" - flagHealthCmd = "health-cmd" - flagHealthInterval = "health-interval" - flagHealthRetries = "health-retries" - flagHealthTimeout = "health-timeout" - flagHealthStartPeriod = "health-start-period" - flagNoHealthcheck = "no-healthcheck" - flagSecret = "secret" - flagSecretAdd = "secret-add" - flagSecretRemove = "secret-rm" -) diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go deleted file mode 100644 index 675fbe4b99..0000000000 --- a/cli/command/service/opts_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package service - -import ( - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/opts" - "github.com/stretchr/testify/assert" -) - -func TestMemBytesString(t *testing.T) { - var mem opts.MemBytes = 1048576 - assert.Equal(t, "1MiB", mem.String()) -} - -func TestMemBytesSetAndValue(t *testing.T) { - var mem opts.MemBytes - assert.NoError(t, mem.Set("5kb")) - assert.Equal(t, int64(5120), mem.Value()) -} - -func TestNanoCPUsString(t *testing.T) { - var cpus opts.NanoCPUs = 6100000000 - assert.Equal(t, "6.100", cpus.String()) -} - -func TestNanoCPUsSetAndValue(t *testing.T) { - var cpus opts.NanoCPUs - assert.NoError(t, cpus.Set("0.35")) - assert.Equal(t, int64(350000000), cpus.Value()) -} - -func TestDurationOptString(t *testing.T) { - dur := time.Duration(300 * 10e8) - duration := DurationOpt{value: &dur} - assert.Equal(t, "5m0s", duration.String()) -} - -func TestDurationOptSetAndValue(t *testing.T) { - var duration DurationOpt - assert.NoError(t, duration.Set("300s")) - assert.Equal(t, time.Duration(300*10e8), *duration.Value()) - assert.NoError(t, duration.Set("-300s")) - assert.Equal(t, time.Duration(-300*10e8), *duration.Value()) -} - -func TestPositiveDurationOptSetAndValue(t *testing.T) { - var duration PositiveDurationOpt - assert.NoError(t, duration.Set("300s")) - assert.Equal(t, time.Duration(300*10e8), *duration.Value()) - assert.EqualError(t, duration.Set("-300s"), "duration cannot be negative") -} - -func TestUint64OptString(t *testing.T) { - value := uint64(2345678) - opt := Uint64Opt{value: &value} - assert.Equal(t, "2345678", opt.String()) - - opt = Uint64Opt{} - assert.Equal(t, "", opt.String()) -} - -func TestUint64OptSetAndValue(t *testing.T) { - var opt Uint64Opt - assert.NoError(t, opt.Set("14445")) - assert.Equal(t, uint64(14445), *opt.Value()) -} - -func TestHealthCheckOptionsToHealthConfig(t *testing.T) { - dur := time.Second - opt := healthCheckOptions{ - cmd: "curl", - interval: PositiveDurationOpt{DurationOpt{value: &dur}}, - timeout: PositiveDurationOpt{DurationOpt{value: &dur}}, - startPeriod: PositiveDurationOpt{DurationOpt{value: &dur}}, - retries: 10, - } - config, err := opt.toHealthConfig() - assert.NoError(t, err) - assert.Equal(t, &container.HealthConfig{ - Test: []string{"CMD-SHELL", "curl"}, - Interval: time.Second, - Timeout: time.Second, - StartPeriod: time.Second, - Retries: 10, - }, config) -} - -func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) { - opt := healthCheckOptions{ - noHealthcheck: true, - } - config, err := opt.toHealthConfig() - assert.NoError(t, err) - assert.Equal(t, &container.HealthConfig{ - Test: []string{"NONE"}, - }, config) -} - -func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { - opt := healthCheckOptions{ - cmd: "curl", - noHealthcheck: true, - } - _, err := opt.toHealthConfig() - assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options") -} diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go deleted file mode 100644 index acee08761f..0000000000 --- a/cli/command/service/parse.go +++ /dev/null @@ -1,59 +0,0 @@ -package service - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - swarmtypes "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -// ParseSecrets retrieves the secrets with the requested names and fills -// secret IDs into the secret references. -func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) { - secretRefs := make(map[string]*swarmtypes.SecretReference) - ctx := context.Background() - - for _, secret := range requestedSecrets { - if _, exists := secretRefs[secret.File.Name]; exists { - return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName) - } - secretRef := new(swarmtypes.SecretReference) - *secretRef = *secret - secretRefs[secret.File.Name] = secretRef - } - - args := filters.NewArgs() - for _, s := range secretRefs { - args.Add("name", s.SecretName) - } - - secrets, err := client.SecretList(ctx, types.SecretListOptions{ - Filters: args, - }) - if err != nil { - return nil, err - } - - foundSecrets := make(map[string]string) - for _, secret := range secrets { - foundSecrets[secret.Spec.Annotations.Name] = secret.ID - } - - addedSecrets := []*swarmtypes.SecretReference{} - - for _, ref := range secretRefs { - id, ok := foundSecrets[ref.SecretName] - if !ok { - return nil, errors.Errorf("secret not found: %s", ref.SecretName) - } - - // set the id for the ref to properly assign in swarm - // since swarm needs the ID instead of the name - ref.SecretID = id - addedSecrets = append(addedSecrets, ref) - } - - return addedSecrets, nil -} diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go deleted file mode 100644 index d68fc6c1af..0000000000 --- a/cli/command/service/progress/progress.go +++ /dev/null @@ -1,409 +0,0 @@ -package progress - -import ( - "errors" - "fmt" - "io" - "os" - "os/signal" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" - "golang.org/x/net/context" -) - -var ( - numberedStates = map[swarm.TaskState]int64{ - swarm.TaskStateNew: 1, - swarm.TaskStateAllocated: 2, - swarm.TaskStatePending: 3, - swarm.TaskStateAssigned: 4, - swarm.TaskStateAccepted: 5, - swarm.TaskStatePreparing: 6, - swarm.TaskStateReady: 7, - swarm.TaskStateStarting: 8, - swarm.TaskStateRunning: 9, - } - - longestState int -) - -const ( - maxProgress = 9 - maxProgressBars = 20 -) - -type progressUpdater interface { - update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) -} - -func init() { - for state := range numberedStates { - if len(state) > longestState { - longestState = len(state) - } - } -} - -func stateToProgress(state swarm.TaskState, rollback bool) int64 { - if !rollback { - return numberedStates[state] - } - return int64(len(numberedStates)) - numberedStates[state] -} - -// ServiceProgress outputs progress information for convergence of a service. -func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error { - defer progressWriter.Close() - - progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false) - - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, os.Interrupt) - defer signal.Stop(sigint) - - taskFilter := filters.NewArgs() - taskFilter.Add("service", serviceID) - taskFilter.Add("_up-to-date", "true") - - getUpToDateTasks := func() ([]swarm.Task, error) { - return client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - } - - var ( - updater progressUpdater - converged bool - convergedAt time.Time - monitor = 5 * time.Second - rollback bool - ) - - for { - service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 { - monitor = service.Spec.UpdateConfig.Monitor - } - - if updater == nil { - updater, err = initializeUpdater(service, progressOut) - if err != nil { - return err - } - } - - if service.UpdateStatus != nil { - switch service.UpdateStatus.State { - case swarm.UpdateStateUpdating: - rollback = false - case swarm.UpdateStateCompleted: - if !converged { - return nil - } - case swarm.UpdateStatePaused: - return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message) - case swarm.UpdateStateRollbackStarted: - if !rollback && service.UpdateStatus.Message != "" { - progressOut.WriteProgress(progress.Progress{ - ID: "rollback", - Action: service.UpdateStatus.Message, - }) - } - rollback = true - case swarm.UpdateStateRollbackPaused: - return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message) - case swarm.UpdateStateRollbackCompleted: - if !converged { - return fmt.Errorf("service rolled back: %s", service.UpdateStatus.Message) - } - } - } - if converged && time.Since(convergedAt) >= monitor { - return nil - } - - tasks, err := getUpToDateTasks() - if err != nil { - return err - } - - activeNodes, err := getActiveNodes(ctx, client) - if err != nil { - return err - } - - converged, err = updater.update(service, tasks, activeNodes, rollback) - if err != nil { - return err - } - if converged { - if convergedAt.IsZero() { - convergedAt = time.Now() - } - wait := monitor - time.Since(convergedAt) - if wait >= 0 { - progressOut.WriteProgress(progress.Progress{ - // Ideally this would have no ID, but - // the progress rendering code behaves - // poorly on an "action" with no ID. It - // returns the cursor to the beginning - // of the line, so the first character - // may be difficult to read. Then the - // output is overwritten by the shell - // prompt when the command finishes. - ID: "verify", - Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1), - }) - } - } else { - if !convergedAt.IsZero() { - progressOut.WriteProgress(progress.Progress{ - ID: "verify", - Action: "Detected task failure", - }) - } - convergedAt = time.Time{} - } - - select { - case <-time.After(200 * time.Millisecond): - case <-sigint: - if !converged { - progress.Message(progressOut, "", "Operation continuing in background.") - progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID) - } - return nil - } - } -} - -func getActiveNodes(ctx context.Context, client client.APIClient) (map[string]swarm.Node, error) { - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return nil, err - } - - activeNodes := make(map[string]swarm.Node) - for _, n := range nodes { - if n.Status.State != swarm.NodeStateDown { - activeNodes[n.ID] = n - } - } - return activeNodes, nil -} - -func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) { - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - return &replicatedProgressUpdater{ - progressOut: progressOut, - }, nil - } - if service.Spec.Mode.Global != nil { - return &globalProgressUpdater{ - progressOut: progressOut, - }, nil - } - return nil, errors.New("unrecognized service mode") -} - -func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) { - if rollback { - progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator), - }) - return - } - progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator), - }) -} - -type replicatedProgressUpdater struct { - progressOut progress.Output - - // used for maping slots to a contiguous space - // this also causes progress bars to appear in order - slotMap map[int]int - - initialized bool - done bool -} - -func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { - if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil { - return false, errors.New("no replica count") - } - replicas := *service.Spec.Mode.Replicated.Replicas - - if !u.initialized { - u.slotMap = make(map[int]int) - - // Draw progress bars in order - writeOverallProgress(u.progressOut, 0, int(replicas), rollback) - - if replicas <= maxProgressBars { - for i := uint64(1); i <= replicas; i++ { - progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ") - } - } - u.initialized = true - } - - // If there are multiple tasks with the same slot number, favor the one - // with the *lowest* desired state. This can happen in restart - // scenarios. - tasksBySlot := make(map[int]swarm.Task) - for _, task := range tasks { - if numberedStates[task.DesiredState] == 0 { - continue - } - if existingTask, ok := tasksBySlot[task.Slot]; ok { - if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { - continue - } - } - if _, nodeActive := activeNodes[task.NodeID]; nodeActive { - tasksBySlot[task.Slot] = task - } - } - - // If we had reached a converged state, check if we are still converged. - if u.done { - for _, task := range tasksBySlot { - if task.Status.State != swarm.TaskStateRunning { - u.done = false - break - } - } - } - - running := uint64(0) - - for _, task := range tasksBySlot { - mappedSlot := u.slotMap[task.Slot] - if mappedSlot == 0 { - mappedSlot = len(u.slotMap) + 1 - u.slotMap[task.Slot] = mappedSlot - } - - if !u.done && replicas <= maxProgressBars && uint64(mappedSlot) <= replicas { - u.progressOut.WriteProgress(progress.Progress{ - ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), - Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), - Current: stateToProgress(task.Status.State, rollback), - Total: maxProgress, - HideCounts: true, - }) - } - if task.Status.State == swarm.TaskStateRunning { - running++ - } - } - - if !u.done { - writeOverallProgress(u.progressOut, int(running), int(replicas), rollback) - - if running == replicas { - u.done = true - } - } - - return running == replicas, nil -} - -type globalProgressUpdater struct { - progressOut progress.Output - - initialized bool - done bool -} - -func (u *globalProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { - // If there are multiple tasks with the same node ID, favor the one - // with the *lowest* desired state. This can happen in restart - // scenarios. - tasksByNode := make(map[string]swarm.Task) - for _, task := range tasks { - if numberedStates[task.DesiredState] == 0 { - continue - } - if existingTask, ok := tasksByNode[task.NodeID]; ok { - if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { - continue - } - } - tasksByNode[task.NodeID] = task - } - - // We don't have perfect knowledge of how many nodes meet the - // constraints for this service. But the orchestrator creates tasks - // for all eligible nodes at the same time, so we should see all those - // nodes represented among the up-to-date tasks. - nodeCount := len(tasksByNode) - - if !u.initialized { - if nodeCount == 0 { - // Two possibilities: either the orchestrator hasn't created - // the tasks yet, or the service doesn't meet constraints for - // any node. Either way, we wait. - u.progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: "waiting for new tasks", - }) - return false, nil - } - - writeOverallProgress(u.progressOut, 0, nodeCount, rollback) - u.initialized = true - } - - // If we had reached a converged state, check if we are still converged. - if u.done { - for _, task := range tasksByNode { - if task.Status.State != swarm.TaskStateRunning { - u.done = false - break - } - } - } - - running := 0 - - for _, task := range tasksByNode { - if node, nodeActive := activeNodes[task.NodeID]; nodeActive { - if !u.done && nodeCount <= maxProgressBars { - u.progressOut.WriteProgress(progress.Progress{ - ID: stringid.TruncateID(node.ID), - Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), - Current: stateToProgress(task.Status.State, rollback), - Total: maxProgress, - HideCounts: true, - }) - } - if task.Status.State == swarm.TaskStateRunning { - running++ - } - } - } - - if !u.done { - writeOverallProgress(u.progressOut, running, nodeCount, rollback) - - if running == nodeCount { - u.done = true - } - } - - return running == nodeCount, nil -} diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go deleted file mode 100644 index 3a53a545d0..0000000000 --- a/cli/command/service/ps.go +++ /dev/null @@ -1,123 +0,0 @@ -package service - -import ( - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/node" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type psOptions struct { - services []string - quiet bool - noResolve bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] SERVICE [SERVICE...]", - Short: "List the tasks of one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.services = args - return runPS(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runPS(dockerCli *command.DockerCli, opts psOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - filter := opts.filter.Value() - - serviceIDFilter := filters.NewArgs() - serviceNameFilter := filters.NewArgs() - for _, service := range opts.services { - serviceIDFilter.Add("id", service) - serviceNameFilter.Add("name", service) - } - serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter}) - if err != nil { - return err - } - serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter}) - if err != nil { - return err - } - - for _, service := range opts.services { - serviceCount := 0 - // Lookup by ID/Prefix - for _, serviceEntry := range serviceByIDList { - if strings.HasPrefix(serviceEntry.ID, service) { - filter.Add("service", serviceEntry.ID) - serviceCount++ - } - } - - // Lookup by Name/Prefix - for _, serviceEntry := range serviceByNameList { - if strings.HasPrefix(serviceEntry.Spec.Annotations.Name, service) { - filter.Add("service", serviceEntry.ID) - serviceCount++ - } - } - // If nothing has been found, return immediately. - if serviceCount == 0 { - return errors.Errorf("no such services: %s", service) - } - } - - if filter.Include("node") { - nodeFilters := filter.Get("node") - for _, nodeFilter := range nodeFilters { - nodeReference, err := node.Reference(ctx, client, nodeFilter) - if err != nil { - return err - } - filter.Del("node", nodeFilter) - filter.Add("node", nodeReference) - } - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) -} diff --git a/cli/command/service/remove.go b/cli/command/service/remove.go deleted file mode 100644 index a7b0107089..0000000000 --- a/cli/command/service/remove.go +++ /dev/null @@ -1,48 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - - cmd := &cobra.Command{ - Use: "rm SERVICE [SERVICE...]", - Aliases: []string{"remove"}, - Short: "Remove one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args) - }, - } - cmd.Flags() - - return cmd -} - -func runRemove(dockerCli *command.DockerCli, sids []string) error { - client := dockerCli.Client() - - ctx := context.Background() - - var errs []string - for _, sid := range sids { - err := client.ServiceRemove(ctx, sid) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", sid) - } - if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/service/scale.go b/cli/command/service/scale.go deleted file mode 100644 index 98163c87c9..0000000000 --- a/cli/command/service/scale.go +++ /dev/null @@ -1,97 +0,0 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ - Use: "scale SERVICE=REPLICAS [SERVICE=REPLICAS...]", - Short: "Scale one or multiple replicated services", - Args: scaleArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runScale(dockerCli, args) - }, - } -} - -func scaleArgs(cmd *cobra.Command, args []string) error { - if err := cli.RequiresMinArgs(1)(cmd, args); err != nil { - return err - } - for _, arg := range args { - if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 { - return errors.Errorf( - "Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s", - arg, - cmd.CommandPath(), - cmd.UseLine(), - cmd.Short, - ) - } - } - return nil -} - -func runScale(dockerCli *command.DockerCli, args []string) error { - var errs []string - for _, arg := range args { - parts := strings.SplitN(arg, "=", 2) - serviceID, scaleStr := parts[0], parts[1] - - // validate input arg scale number - scale, err := strconv.ParseUint(scaleStr, 10, 64) - if err != nil { - errs = append(errs, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err)) - continue - } - - if err := runServiceScale(dockerCli, serviceID, scale); err != nil { - errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err)) - } - } - - if len(errs) == 0 { - return nil - } - return errors.Errorf(strings.Join(errs, "\n")) -} - -func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint64) error { - client := dockerCli.Client() - ctx := context.Background() - - service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - serviceMode := &service.Spec.Mode - if serviceMode.Replicated == nil { - return errors.Errorf("scale can only be used with replicated mode") - } - - serviceMode.Replicated.Replicas = &scale - - response, err := client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale) - return nil -} diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go deleted file mode 100644 index eba52a9dd1..0000000000 --- a/cli/command/service/trust.go +++ /dev/null @@ -1,87 +0,0 @@ -package service - -import ( - "encoding/hex" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/registry" - "github.com/docker/notary/tuf/data" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error { - if !command.IsTrusted() { - // Digests are resolved by the daemon when not using content - // trust. - return nil - } - - ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image) - if err != nil { - return errors.Wrapf(err, "invalid reference %s", service.TaskTemplate.ContainerSpec.Image) - } - - // If reference does not have digest (is not canonical nor image id) - if _, ok := ref.(reference.Digested); !ok { - namedRef, ok := ref.(reference.Named) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not named") - } - namedRef = reference.TagNameOnly(namedRef) - taggedRef, ok := namedRef.(reference.NamedTagged) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not tagged") - } - - resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) - if err != nil { - return errors.Wrap(err, "failed to resolve image digest using content trust") - } - resolvedFamiliar := reference.FamiliarString(resolvedImage) - logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar) - service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar - } - - return nil -} - -func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - - authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - return nil, errors.Wrap(err, "error establishing connection to trust repository") - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(repoInfo.Name.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref))) - } - - logrus.Debugf("retrieving target for %s role\n", t.Role) - h, ok := t.Hashes["sha256"] - if !ok { - return nil, errors.New("no valid hash, expecting sha256") - } - - dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) - - // Allow returning canonical reference with tag - return reference.WithDigest(ref, dgst) -} diff --git a/cli/command/service/update.go b/cli/command/service/update.go deleted file mode 100644 index 233da68eee..0000000000 --- a/cli/command/service/update.go +++ /dev/null @@ -1,1018 +0,0 @@ -package service - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" - "github.com/docker/swarmkit/api/defaults" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { - serviceOpts := newServiceOptions() - - cmd := &cobra.Command{ - Use: "update [OPTIONS] SERVICE", - Short: "Update a service", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), serviceOpts, args[0]) - }, - } - - flags := cmd.Flags() - flags.String("image", "", "Service image tag") - flags.Var(&ShlexOpt{}, "args", "Service command args") - flags.Bool("rollback", false, "Rollback to previous specification") - flags.SetAnnotation("rollback", "version", []string{"1.25"}) - flags.Bool("force", false, "Force update even if no changes require it") - flags.SetAnnotation("force", "version", []string{"1.25"}) - addServiceFlags(flags, serviceOpts, nil) - - flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") - flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") - flags.SetAnnotation(flagGroupRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") - flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") - flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") - // flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port") - flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port") - flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") - flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") - flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") - flags.SetAnnotation(flagDNSOptionRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") - flags.SetAnnotation(flagDNSSearchRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") - flags.SetAnnotation(flagHostRemove, "version", []string{"1.25"}) - flags.Var(&serviceOpts.labels, flagLabelAdd, "Add or update a service label") - flags.Var(&serviceOpts.containerLabels, flagContainerLabelAdd, "Add or update a container label") - flags.Var(&serviceOpts.env, flagEnvAdd, "Add or update an environment variable") - flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) - flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service") - flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") - flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") - flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference") - flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) - flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") - flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) - flags.Var(&serviceOpts.networks, flagNetworkAdd, "Add a network") - flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"}) - flags.Var(newListOptsVar(), flagNetworkRemove, "Remove a network") - flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"}) - flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") - flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") - flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dns, flagDNSAdd, "Add or update a custom DNS server") - flags.SetAnnotation(flagDNSAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") - flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") - flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) - - return cmd -} - -func newListOptsVar() *opts.ListOpts { - return opts.NewListOptsRef(&[]string{}, nil) -} - -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions, serviceID string) error { - apiClient := dockerCli.Client() - ctx := context.Background() - - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - rollback, err := flags.GetBool("rollback") - if err != nil { - return err - } - - // There are two ways to do user-requested rollback. The old way is - // client-side, but with a sufficiently recent daemon we prefer - // server-side, because it will honor the rollback parameters. - var ( - clientSideRollback bool - serverSideRollback bool - ) - - spec := &service.Spec - if rollback { - // Rollback can't be combined with other flags. - otherFlagsPassed := false - flags.VisitAll(func(f *pflag.Flag) { - if f.Name == "rollback" { - return - } - if flags.Changed(f.Name) { - otherFlagsPassed = true - } - }) - if otherFlagsPassed { - return errors.New("other flags may not be combined with --rollback") - } - - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.28") { - clientSideRollback = true - spec = service.PreviousSpec - if spec == nil { - return errors.Errorf("service does not have a previous specification to roll back to") - } - } else { - serverSideRollback = true - } - } - - updateOpts := types.ServiceUpdateOptions{} - if serverSideRollback { - updateOpts.Rollback = "previous" - } - - err = updateService(ctx, apiClient, flags, spec) - if err != nil { - return err - } - - if flags.Changed("image") { - if err := resolveServiceImageDigest(dockerCli, spec); err != nil { - return err - } - } - - updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) - if err != nil { - return err - } - - spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets - - // only send auth if flag was set - sendAuth, err := flags.GetBool(flagRegistryAuth) - if err != nil { - return err - } - if sendAuth { - // Retrieve encoded auth token from the image reference - // This would be the old image if it didn't change in this update - image := spec.TaskTemplate.ContainerSpec.Image - encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) - if err != nil { - return err - } - updateOpts.EncodedRegistryAuth = encodedAuth - } else if clientSideRollback { - updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec - } else { - updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec - } - - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) - - if opts.detach { - if !flags.Changed("detach") { - fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be updated in the background.\n"+ - "In a future release, --detach=false will become the default.") - } - return nil - } - - return waitOnService(ctx, dockerCli, serviceID, opts) -} - -func updateService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { - updateString := func(flag string, field *string) { - if flags.Changed(flag) { - *field, _ = flags.GetString(flag) - } - } - - updateInt64Value := func(flag string, field *int64) { - if flags.Changed(flag) { - *field = flags.Lookup(flag).Value.(int64Value).Value() - } - } - - updateFloatValue := func(flag string, field *float32) { - if flags.Changed(flag) { - *field = flags.Lookup(flag).Value.(*floatValue).Value() - } - } - - updateDuration := func(flag string, field *time.Duration) { - if flags.Changed(flag) { - *field, _ = flags.GetDuration(flag) - } - } - - updateDurationOpt := func(flag string, field **time.Duration) { - if flags.Changed(flag) { - val := *flags.Lookup(flag).Value.(*DurationOpt).Value() - *field = &val - } - } - - updateUint64 := func(flag string, field *uint64) { - if flags.Changed(flag) { - *field, _ = flags.GetUint64(flag) - } - } - - updateUint64Opt := func(flag string, field **uint64) { - if flags.Changed(flag) { - val := *flags.Lookup(flag).Value.(*Uint64Opt).Value() - *field = &val - } - } - - cspec := &spec.TaskTemplate.ContainerSpec - task := &spec.TaskTemplate - - taskResources := func() *swarm.ResourceRequirements { - if task.Resources == nil { - task.Resources = &swarm.ResourceRequirements{} - } - return task.Resources - } - - updateLabels(flags, &spec.Labels) - updateContainerLabels(flags, &cspec.Labels) - updateString("image", &cspec.Image) - updateStringToSlice(flags, "args", &cspec.Args) - updateStringToSlice(flags, flagEntrypoint, &cspec.Command) - updateEnvironment(flags, &cspec.Env) - updateString(flagWorkdir, &cspec.Dir) - updateString(flagUser, &cspec.User) - updateString(flagHostname, &cspec.Hostname) - if err := updateMounts(flags, &cspec.Mounts); err != nil { - return err - } - - if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) { - taskResources().Limits = &swarm.Resources{} - updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs) - updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes) - } - if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) { - taskResources().Reservations = &swarm.Resources{} - updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs) - updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) - } - - updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod) - - if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) { - if task.RestartPolicy == nil { - task.RestartPolicy = defaultRestartPolicy() - } - if flags.Changed(flagRestartCondition) { - value, _ := flags.GetString(flagRestartCondition) - task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value) - } - updateDurationOpt(flagRestartDelay, &task.RestartPolicy.Delay) - updateUint64Opt(flagRestartMaxAttempts, &task.RestartPolicy.MaxAttempts) - updateDurationOpt(flagRestartWindow, &task.RestartPolicy.Window) - } - - if anyChanged(flags, flagConstraintAdd, flagConstraintRemove) { - if task.Placement == nil { - task.Placement = &swarm.Placement{} - } - updatePlacementConstraints(flags, task.Placement) - } - - if anyChanged(flags, flagPlacementPrefAdd, flagPlacementPrefRemove) { - if task.Placement == nil { - task.Placement = &swarm.Placement{} - } - updatePlacementPreferences(flags, task.Placement) - } - - if anyChanged(flags, flagNetworkAdd, flagNetworkRemove) { - if err := updateNetworks(ctx, apiClient, flags, spec); err != nil { - return err - } - } - - if err := updateReplicas(flags, &spec.Mode); err != nil { - return err - } - - if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio, flagUpdateOrder) { - if spec.UpdateConfig == nil { - spec.UpdateConfig = updateConfigFromDefaults(defaults.Service.Update) - } - updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) - updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) - updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor) - updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) - updateFloatValue(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) - updateString(flagUpdateOrder, &spec.UpdateConfig.Order) - } - - if anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio, flagRollbackOrder) { - if spec.RollbackConfig == nil { - spec.RollbackConfig = updateConfigFromDefaults(defaults.Service.Rollback) - } - updateUint64(flagRollbackParallelism, &spec.RollbackConfig.Parallelism) - updateDuration(flagRollbackDelay, &spec.RollbackConfig.Delay) - updateDuration(flagRollbackMonitor, &spec.RollbackConfig.Monitor) - updateString(flagRollbackFailureAction, &spec.RollbackConfig.FailureAction) - updateFloatValue(flagRollbackMaxFailureRatio, &spec.RollbackConfig.MaxFailureRatio) - updateString(flagRollbackOrder, &spec.RollbackConfig.Order) - } - - if flags.Changed(flagEndpointMode) { - value, _ := flags.GetString(flagEndpointMode) - if spec.EndpointSpec == nil { - spec.EndpointSpec = &swarm.EndpointSpec{} - } - spec.EndpointSpec.Mode = swarm.ResolutionMode(value) - } - - if anyChanged(flags, flagGroupAdd, flagGroupRemove) { - if err := updateGroups(flags, &cspec.Groups); err != nil { - return err - } - } - - if anyChanged(flags, flagPublishAdd, flagPublishRemove) { - if spec.EndpointSpec == nil { - spec.EndpointSpec = &swarm.EndpointSpec{} - } - if err := updatePorts(flags, &spec.EndpointSpec.Ports); err != nil { - return err - } - } - - if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionAdd, flagDNSOptionRemove, flagDNSSearchAdd, flagDNSSearchRemove) { - if cspec.DNSConfig == nil { - cspec.DNSConfig = &swarm.DNSConfig{} - } - if err := updateDNSConfig(flags, &cspec.DNSConfig); err != nil { - return err - } - } - - if anyChanged(flags, flagHostAdd, flagHostRemove) { - if err := updateHosts(flags, &cspec.Hosts); err != nil { - return err - } - } - - if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { - return err - } - - force, err := flags.GetBool("force") - if err != nil { - return err - } - - if force { - spec.TaskTemplate.ForceUpdate++ - } - - if err := updateHealthcheck(flags, cspec); err != nil { - return err - } - - if flags.Changed(flagTTY) { - tty, err := flags.GetBool(flagTTY) - if err != nil { - return err - } - cspec.TTY = tty - } - - if flags.Changed(flagReadOnly) { - readOnly, err := flags.GetBool(flagReadOnly) - if err != nil { - return err - } - cspec.ReadOnly = readOnly - } - - updateString(flagStopSignal, &cspec.StopSignal) - - return nil -} - -func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) { - if !flags.Changed(flag) { - return - } - - *field = flags.Lookup(flag).Value.(*ShlexOpt).Value() -} - -func anyChanged(flags *pflag.FlagSet, fields ...string) bool { - for _, flag := range fields { - if flags.Changed(flag) { - return true - } - } - return false -} - -func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) { - if flags.Changed(flagConstraintAdd) { - values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll() - placement.Constraints = append(placement.Constraints, values...) - } - toRemove := buildToRemoveSet(flags, flagConstraintRemove) - - newConstraints := []string{} - for _, constraint := range placement.Constraints { - if _, exists := toRemove[constraint]; !exists { - newConstraints = append(newConstraints, constraint) - } - } - // Sort so that result is predictable. - sort.Strings(newConstraints) - - placement.Constraints = newConstraints -} - -func updatePlacementPreferences(flags *pflag.FlagSet, placement *swarm.Placement) { - var newPrefs []swarm.PlacementPreference - - if flags.Changed(flagPlacementPrefRemove) { - for _, existing := range placement.Preferences { - removed := false - for _, removal := range flags.Lookup(flagPlacementPrefRemove).Value.(*placementPrefOpts).prefs { - if removal.Spread != nil && existing.Spread != nil && removal.Spread.SpreadDescriptor == existing.Spread.SpreadDescriptor { - removed = true - break - } - } - if !removed { - newPrefs = append(newPrefs, existing) - } - } - } else { - newPrefs = placement.Preferences - } - - if flags.Changed(flagPlacementPrefAdd) { - for _, addition := range flags.Lookup(flagPlacementPrefAdd).Value.(*placementPrefOpts).prefs { - newPrefs = append(newPrefs, addition) - } - } - - placement.Preferences = newPrefs -} - -func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) { - if flags.Changed(flagContainerLabelAdd) { - if *field == nil { - *field = map[string]string{} - } - - values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetAll() - for key, value := range runconfigopts.ConvertKVStringsToMap(values) { - (*field)[key] = value - } - } - - if *field != nil && flags.Changed(flagContainerLabelRemove) { - toRemove := flags.Lookup(flagContainerLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, label := range toRemove { - delete(*field, label) - } - } -} - -func updateLabels(flags *pflag.FlagSet, field *map[string]string) { - if flags.Changed(flagLabelAdd) { - if *field == nil { - *field = map[string]string{} - } - - values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() - for key, value := range runconfigopts.ConvertKVStringsToMap(values) { - (*field)[key] = value - } - } - - if *field != nil && flags.Changed(flagLabelRemove) { - toRemove := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, label := range toRemove { - delete(*field, label) - } - } -} - -func updateEnvironment(flags *pflag.FlagSet, field *[]string) { - if flags.Changed(flagEnvAdd) { - envSet := map[string]string{} - for _, v := range *field { - envSet[envKey(v)] = v - } - - value := flags.Lookup(flagEnvAdd).Value.(*opts.ListOpts) - for _, v := range value.GetAll() { - envSet[envKey(v)] = v - } - - *field = []string{} - for _, v := range envSet { - *field = append(*field, v) - } - } - - toRemove := buildToRemoveSet(flags, flagEnvRemove) - *field = removeItems(*field, toRemove, envKey) -} - -func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { - newSecrets := []*swarm.SecretReference{} - - toRemove := buildToRemoveSet(flags, flagSecretRemove) - for _, secret := range secrets { - if _, exists := toRemove[secret.SecretName]; !exists { - newSecrets = append(newSecrets, secret) - } - } - - if flags.Changed(flagSecretAdd) { - values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() - - addSecrets, err := ParseSecrets(apiClient, values) - if err != nil { - return nil, err - } - newSecrets = append(newSecrets, addSecrets...) - } - - return newSecrets, nil -} - -func envKey(value string) string { - kv := strings.SplitN(value, "=", 2) - return kv[0] -} - -func itemKey(value string) string { - return value -} - -func buildToRemoveSet(flags *pflag.FlagSet, flag string) map[string]struct{} { - var empty struct{} - toRemove := make(map[string]struct{}) - - if !flags.Changed(flag) { - return toRemove - } - - toRemoveSlice := flags.Lookup(flag).Value.(*opts.ListOpts).GetAll() - for _, key := range toRemoveSlice { - toRemove[key] = empty - } - return toRemove -} - -func removeItems( - seq []string, - toRemove map[string]struct{}, - keyFunc func(string) string, -) []string { - newSeq := []string{} - for _, item := range seq { - if _, exists := toRemove[keyFunc(item)]; !exists { - newSeq = append(newSeq, item) - } - } - return newSeq -} - -type byMountSource []mounttypes.Mount - -func (m byMountSource) Len() int { return len(m) } -func (m byMountSource) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m byMountSource) Less(i, j int) bool { - a, b := m[i], m[j] - - if a.Source == b.Source { - return a.Target < b.Target - } - - return a.Source < b.Source -} - -func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { - mountsByTarget := map[string]mounttypes.Mount{} - - if flags.Changed(flagMountAdd) { - values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() - for _, mount := range values { - if _, ok := mountsByTarget[mount.Target]; ok { - return errors.Errorf("duplicate mount target") - } - mountsByTarget[mount.Target] = mount - } - } - - // Add old list of mount points minus updated one. - for _, mount := range *mounts { - if _, ok := mountsByTarget[mount.Target]; !ok { - mountsByTarget[mount.Target] = mount - } - } - - newMounts := []mounttypes.Mount{} - - toRemove := buildToRemoveSet(flags, flagMountRemove) - - for _, mount := range mountsByTarget { - if _, exists := toRemove[mount.Target]; !exists { - newMounts = append(newMounts, mount) - } - } - sort.Sort(byMountSource(newMounts)) - *mounts = newMounts - return nil -} - -func updateGroups(flags *pflag.FlagSet, groups *[]string) error { - if flags.Changed(flagGroupAdd) { - values := flags.Lookup(flagGroupAdd).Value.(*opts.ListOpts).GetAll() - *groups = append(*groups, values...) - } - toRemove := buildToRemoveSet(flags, flagGroupRemove) - - newGroups := []string{} - for _, group := range *groups { - if _, exists := toRemove[group]; !exists { - newGroups = append(newGroups, group) - } - } - // Sort so that result is predictable. - sort.Strings(newGroups) - - *groups = newGroups - return nil -} - -func removeDuplicates(entries []string) []string { - hit := map[string]bool{} - newEntries := []string{} - for _, v := range entries { - if !hit[v] { - newEntries = append(newEntries, v) - hit[v] = true - } - } - return newEntries -} - -func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { - newConfig := &swarm.DNSConfig{} - - nameservers := (*config).Nameservers - if flags.Changed(flagDNSAdd) { - values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetAll() - nameservers = append(nameservers, values...) - } - nameservers = removeDuplicates(nameservers) - toRemove := buildToRemoveSet(flags, flagDNSRemove) - for _, nameserver := range nameservers { - if _, exists := toRemove[nameserver]; !exists { - newConfig.Nameservers = append(newConfig.Nameservers, nameserver) - - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Nameservers) - - search := (*config).Search - if flags.Changed(flagDNSSearchAdd) { - values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetAll() - search = append(search, values...) - } - search = removeDuplicates(search) - toRemove = buildToRemoveSet(flags, flagDNSSearchRemove) - for _, entry := range search { - if _, exists := toRemove[entry]; !exists { - newConfig.Search = append(newConfig.Search, entry) - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Search) - - options := (*config).Options - if flags.Changed(flagDNSOptionAdd) { - values := flags.Lookup(flagDNSOptionAdd).Value.(*opts.ListOpts).GetAll() - options = append(options, values...) - } - options = removeDuplicates(options) - toRemove = buildToRemoveSet(flags, flagDNSOptionRemove) - for _, option := range options { - if _, exists := toRemove[option]; !exists { - newConfig.Options = append(newConfig.Options, option) - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Options) - - *config = newConfig - return nil -} - -type byPortConfig []swarm.PortConfig - -func (r byPortConfig) Len() int { return len(r) } -func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byPortConfig) Less(i, j int) bool { - // We convert PortConfig into `port/protocol`, e.g., `80/tcp` - // In updatePorts we already filter out with map so there is duplicate entries - return portConfigToString(&r[i]) < portConfigToString(&r[j]) -} - -func portConfigToString(portConfig *swarm.PortConfig) string { - protocol := portConfig.Protocol - mode := portConfig.PublishMode - return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) -} - -func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { - // The key of the map is `port/protocol`, e.g., `80/tcp` - portSet := map[string]swarm.PortConfig{} - - // Build the current list of portConfig - for _, entry := range *portConfig { - if _, ok := portSet[portConfigToString(&entry)]; !ok { - portSet[portConfigToString(&entry)] = entry - } - } - - newPorts := []swarm.PortConfig{} - - // Clean current ports - toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value() -portLoop: - for _, port := range portSet { - for _, pConfig := range toRemove { - if equalProtocol(port.Protocol, pConfig.Protocol) && - port.TargetPort == pConfig.TargetPort && - equalPublishMode(port.PublishMode, pConfig.PublishMode) { - continue portLoop - } - } - - newPorts = append(newPorts, port) - } - - // Check to see if there are any conflict in flags. - if flags.Changed(flagPublishAdd) { - ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() - - for _, port := range ports { - if _, ok := portSet[portConfigToString(&port)]; ok { - continue - } - //portSet[portConfigToString(&port)] = port - newPorts = append(newPorts, port) - } - } - - // Sort the PortConfig to avoid unnecessary updates - sort.Sort(byPortConfig(newPorts)) - *portConfig = newPorts - return nil -} - -func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool { - return prot1 == prot2 || - (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) || - (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP) -} - -func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool { - return mode1 == mode2 || - (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) || - (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress) -} - -func equalPort(targetPort nat.Port, port swarm.PortConfig) bool { - return (string(port.Protocol) == targetPort.Proto() && - port.TargetPort == uint32(targetPort.Int())) -} - -func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error { - if !flags.Changed(flagReplicas) { - return nil - } - - if serviceMode == nil || serviceMode.Replicated == nil { - return errors.Errorf("replicas can only be used with replicated mode") - } - serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value() - return nil -} - -func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { - // Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format) - if flags.Changed(flagHostAdd) { - values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll()) - *hosts = append(*hosts, values...) - } - // Remove duplicate - *hosts = removeDuplicates(*hosts) - - keysToRemove := make(map[string]struct{}) - if flags.Changed(flagHostRemove) { - var empty struct{} - extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll() - for _, entry := range extraHostsToRemove { - key := strings.SplitN(entry, ":", 2)[0] - keysToRemove[key] = empty - } - } - - newHosts := []string{} - for _, entry := range *hosts { - // Since this is in swarmkit format, we need to find the key, which is canonical_hostname of: - // IP_address canonical_hostname [aliases...] - parts := strings.Fields(entry) - if len(parts) > 1 { - key := parts[1] - if _, exists := keysToRemove[key]; !exists { - newHosts = append(newHosts, entry) - } - } else { - newHosts = append(newHosts, entry) - } - } - - // Sort so that result is predictable. - sort.Strings(newHosts) - - *hosts = newHosts - return nil -} - -// updateLogDriver updates the log driver only if the log driver flag is set. -// All options will be replaced with those provided on the command line. -func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { - if !flags.Changed(flagLogDriver) { - return nil - } - - name, err := flags.GetString(flagLogDriver) - if err != nil { - return err - } - - if name == "" { - return nil - } - - taskTemplate.LogDriver = &swarm.Driver{ - Name: name, - Options: runconfigopts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()), - } - - return nil -} - -func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error { - if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { - return nil - } - if containerSpec.Healthcheck == nil { - containerSpec.Healthcheck = &container.HealthConfig{} - } - noHealthcheck, err := flags.GetBool(flagNoHealthcheck) - if err != nil { - return err - } - if noHealthcheck { - if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { - containerSpec.Healthcheck = &container.HealthConfig{ - Test: []string{"NONE"}, - } - return nil - } - return errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) - } - if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" { - containerSpec.Healthcheck.Test = nil - } - if flags.Changed(flagHealthInterval) { - val := *flags.Lookup(flagHealthInterval).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.Interval = val - } - if flags.Changed(flagHealthTimeout) { - val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.Timeout = val - } - if flags.Changed(flagHealthStartPeriod) { - val := *flags.Lookup(flagHealthStartPeriod).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.StartPeriod = val - } - if flags.Changed(flagHealthRetries) { - containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries) - } - if flags.Changed(flagHealthCmd) { - cmd, _ := flags.GetString(flagHealthCmd) - if cmd != "" { - containerSpec.Healthcheck.Test = []string{"CMD-SHELL", cmd} - } else { - containerSpec.Healthcheck.Test = nil - } - } - return nil -} - -type byNetworkTarget []swarm.NetworkAttachmentConfig - -func (m byNetworkTarget) Len() int { return len(m) } -func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m byNetworkTarget) Less(i, j int) bool { - return m[i].Target < m[j].Target -} - -func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { - // spec.TaskTemplate.Networks takes precedence over the deprecated - // spec.Networks field. If spec.Network is in use, we'll migrate those - // values to spec.TaskTemplate.Networks. - specNetworks := spec.TaskTemplate.Networks - if len(specNetworks) == 0 { - specNetworks = spec.Networks - } - spec.Networks = nil - - toRemove := buildToRemoveSet(flags, flagNetworkRemove) - idsToRemove := make(map[string]struct{}) - for networkIDOrName := range toRemove { - network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false) - if err != nil { - return err - } - idsToRemove[network.ID] = struct{}{} - } - - existingNetworks := make(map[string]struct{}) - var newNetworks []swarm.NetworkAttachmentConfig - for _, network := range specNetworks { - if _, exists := idsToRemove[network.Target]; exists { - continue - } - - newNetworks = append(newNetworks, network) - existingNetworks[network.Target] = struct{}{} - } - - if flags.Changed(flagNetworkAdd) { - values := flags.Lookup(flagNetworkAdd).Value.(*opts.ListOpts).GetAll() - networks, err := convertNetworks(ctx, apiClient, values) - if err != nil { - return err - } - for _, network := range networks { - if _, exists := existingNetworks[network.Target]; exists { - return errors.Errorf("service is already attached to network %s", network.Target) - } - newNetworks = append(newNetworks, network) - existingNetworks[network.Target] = struct{}{} - } - } - - sort.Sort(byNetworkTarget(newNetworks)) - - spec.TaskTemplate.Networks = newNetworks - return nil -} diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go deleted file mode 100644 index 8f49d52ab8..0000000000 --- a/cli/command/service/update_test.go +++ /dev/null @@ -1,496 +0,0 @@ -package service - -import ( - "reflect" - "sort" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/context" -) - -func TestUpdateServiceArgs(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("args", "the \"new args\"") - - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - cspec.Args = []string{"old", "args"} - - updateService(nil, nil, flags, spec) - assert.Equal(t, []string{"the", "new args"}, cspec.Args) -} - -func TestUpdateLabels(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("label-add", "toadd=newlabel") - flags.Set("label-rm", "toremove") - - labels := map[string]string{ - "toremove": "thelabeltoremove", - "tokeep": "value", - } - - updateLabels(flags, &labels) - assert.Len(t, labels, 2) - assert.Equal(t, "value", labels["tokeep"]) - assert.Equal(t, "newlabel", labels["toadd"]) -} - -func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("label-rm", "dne") - - labels := map[string]string{"foo": "theoldlabel"} - updateLabels(flags, &labels) - assert.Len(t, labels, 1) -} - -func TestUpdatePlacementConstraints(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("constraint-add", "node=toadd") - flags.Set("constraint-rm", "node!=toremove") - - placement := &swarm.Placement{ - Constraints: []string{"node!=toremove", "container=tokeep"}, - } - - updatePlacementConstraints(flags, placement) - require.Len(t, placement.Constraints, 2) - assert.Equal(t, "container=tokeep", placement.Constraints[0]) - assert.Equal(t, "node=toadd", placement.Constraints[1]) -} - -func TestUpdatePlacementPrefs(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("placement-pref-add", "spread=node.labels.dc") - flags.Set("placement-pref-rm", "spread=node.labels.rack") - - placement := &swarm.Placement{ - Preferences: []swarm.PlacementPreference{ - { - Spread: &swarm.SpreadOver{ - SpreadDescriptor: "node.labels.rack", - }, - }, - { - Spread: &swarm.SpreadOver{ - SpreadDescriptor: "node.labels.row", - }, - }, - }, - } - - updatePlacementPreferences(flags, placement) - require.Len(t, placement.Preferences, 2) - assert.Equal(t, "node.labels.row", placement.Preferences[0].Spread.SpreadDescriptor) - assert.Equal(t, "node.labels.dc", placement.Preferences[1].Spread.SpreadDescriptor) -} - -func TestUpdateEnvironment(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "toadd=newenv") - flags.Set("env-rm", "toremove") - - envs := []string{"toremove=theenvtoremove", "tokeep=value"} - - updateEnvironment(flags, &envs) - require.Len(t, envs, 2) - // Order has been removed in updateEnvironment (map) - sort.Strings(envs) - assert.Equal(t, "toadd=newenv", envs[0]) - assert.Equal(t, "tokeep=value", envs[1]) -} - -func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "foo=newenv") - flags.Set("env-add", "foo=dupe") - flags.Set("env-rm", "foo") - - envs := []string{"foo=value"} - - updateEnvironment(flags, &envs) - assert.Len(t, envs, 0) -} - -func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) { - // Test case for #25404 - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "A=b") - - envs := []string{"A=c"} - - updateEnvironment(flags, &envs) - require.Len(t, envs, 1) - assert.Equal(t, "A=b", envs[0]) -} - -func TestUpdateGroups(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("group-add", "wheel") - flags.Set("group-add", "docker") - flags.Set("group-rm", "root") - flags.Set("group-add", "foo") - flags.Set("group-rm", "docker") - - groups := []string{"bar", "root"} - - updateGroups(flags, &groups) - require.Len(t, groups, 3) - assert.Equal(t, "bar", groups[0]) - assert.Equal(t, "foo", groups[1]) - assert.Equal(t, "wheel", groups[2]) -} - -func TestUpdateDNSConfig(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - - // IPv4, with duplicates - flags.Set("dns-add", "1.1.1.1") - flags.Set("dns-add", "1.1.1.1") - flags.Set("dns-add", "2.2.2.2") - flags.Set("dns-rm", "3.3.3.3") - flags.Set("dns-rm", "2.2.2.2") - // IPv6 - flags.Set("dns-add", "2001:db8:abc8::1") - // Invalid dns record - assert.EqualError(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address") - - // domains with duplicates - flags.Set("dns-search-add", "example.com") - flags.Set("dns-search-add", "example.com") - flags.Set("dns-search-add", "example.org") - flags.Set("dns-search-rm", "example.org") - // Invalid dns search domain - assert.EqualError(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain") - - flags.Set("dns-option-add", "ndots:9") - flags.Set("dns-option-rm", "timeout:3") - - config := &swarm.DNSConfig{ - Nameservers: []string{"3.3.3.3", "5.5.5.5"}, - Search: []string{"localdomain"}, - Options: []string{"timeout:3"}, - } - - updateDNSConfig(flags, &config) - - require.Len(t, config.Nameservers, 3) - assert.Equal(t, "1.1.1.1", config.Nameservers[0]) - assert.Equal(t, "2001:db8:abc8::1", config.Nameservers[1]) - assert.Equal(t, "5.5.5.5", config.Nameservers[2]) - - require.Len(t, config.Search, 2) - assert.Equal(t, "example.com", config.Search[0]) - assert.Equal(t, "localdomain", config.Search[1]) - - require.Len(t, config.Options, 1) - assert.Equal(t, config.Options[0], "ndots:9") -} - -func TestUpdateMounts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("mount-add", "type=volume,source=vol2,target=/toadd") - flags.Set("mount-rm", "/toremove") - - mounts := []mounttypes.Mount{ - {Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind}, - } - - updateMounts(flags, &mounts) - require.Len(t, mounts, 2) - assert.Equal(t, "/toadd", mounts[0].Target) - assert.Equal(t, "/tokeep", mounts[1].Target) -} - -func TestUpdateMountsWithDuplicateMounts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("mount-add", "type=volume,source=vol4,target=/toadd") - - mounts := []mounttypes.Mount{ - {Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind}, - {Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind}, - } - - updateMounts(flags, &mounts) - require.Len(t, mounts, 3) - assert.Equal(t, "/tokeep1", mounts[0].Target) - assert.Equal(t, "/tokeep2", mounts[1].Target) - assert.Equal(t, "/toadd", mounts[2].Target) -} - -func TestUpdatePorts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "1000:1000") - flags.Set("publish-rm", "333/udp") - - portConfigs := []swarm.PortConfig{ - {TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP}, - {TargetPort: 555}, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 2) - // Do a sort to have the order (might have changed by map) - targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)} - sort.Ints(targetPorts) - assert.Equal(t, 555, targetPorts[0]) - assert.Equal(t, 1000, targetPorts[1]) -} - -func TestUpdatePortsDuplicate(t *testing.T) { - // Test case for #25375 - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:80") - - portConfigs := []swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 80, - Protocol: swarm.PortConfigProtocolTCP, - PublishMode: swarm.PortConfigPublishModeIngress, - }, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 1) - assert.Equal(t, uint32(80), portConfigs[0].TargetPort) -} - -func TestUpdateHealthcheckTable(t *testing.T) { - type test struct { - flags [][2]string - initial *container.HealthConfig - expected *container.HealthConfig - err string - } - testCases := []test{ - { - flags: [][2]string{{"no-healthcheck", "true"}}, - initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Test: []string{"NONE"}}, - }, - { - flags: [][2]string{{"health-cmd", "cmd1"}}, - initial: &container.HealthConfig{Test: []string{"NONE"}}, - expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}}, - }, - { - flags: [][2]string{{"health-retries", "10"}}, - initial: &container.HealthConfig{Test: []string{"NONE"}}, - expected: &container.HealthConfig{Retries: 10}, - }, - { - flags: [][2]string{{"health-retries", "10"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - }, - { - flags: [][2]string{{"health-interval", "1m"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute}, - }, - { - flags: [][2]string{{"health-cmd", ""}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Retries: 10}, - }, - { - flags: [][2]string{{"health-retries", "0"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - }, - { - flags: [][2]string{{"health-start-period", "1m"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute}, - }, - { - flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - { - flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - { - flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - } - for i, c := range testCases { - flags := newUpdateCommand(nil).Flags() - for _, flag := range c.flags { - flags.Set(flag[0], flag[1]) - } - cspec := &swarm.ContainerSpec{ - Healthcheck: c.initial, - } - err := updateHealthcheck(flags, cspec) - if c.err != "" { - assert.EqualError(t, err, c.err) - } else { - assert.NoError(t, err) - if !reflect.DeepEqual(cspec.Healthcheck, c.expected) { - t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck) - } - } - } -} - -func TestUpdateHosts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("host-add", "example.net:2.2.2.2") - flags.Set("host-add", "ipv6.net:2001:db8:abc8::1") - // remove with ipv6 should work - flags.Set("host-rm", "example.net:2001:db8:abc8::1") - // just hostname should work as well - flags.Set("host-rm", "example.net") - // bad format error - assert.EqualError(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`) - - hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"} - - updateHosts(flags, &hosts) - require.Len(t, hosts, 3) - assert.Equal(t, "1.2.3.4 example.com", hosts[0]) - assert.Equal(t, "2001:db8:abc8::1 ipv6.net", hosts[1]) - assert.Equal(t, "4.3.2.1 example.org", hosts[2]) -} - -func TestUpdatePortsRmWithProtocol(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "8081:81") - flags.Set("publish-add", "8082:82") - flags.Set("publish-rm", "80") - flags.Set("publish-rm", "81/tcp") - flags.Set("publish-rm", "82/udp") - - portConfigs := []swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 8080, - Protocol: swarm.PortConfigProtocolTCP, - PublishMode: swarm.PortConfigPublishModeIngress, - }, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 2) - assert.Equal(t, uint32(81), portConfigs[0].TargetPort) - assert.Equal(t, uint32(82), portConfigs[1].TargetPort) -} - -type secretAPIClientMock struct { - listResult []swarm.Secret -} - -func (s secretAPIClientMock) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - return s.listResult, nil -} -func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { - return types.SecretCreateResponse{}, nil -} -func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error { - return nil -} -func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, []byte{}, nil -} -func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { - return nil -} - -// TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update" -// by combining "--secret-rm" and "--secret-add" for the same secret. -func TestUpdateSecretUpdateInPlace(t *testing.T) { - apiClient := secretAPIClientMock{ - listResult: []swarm.Secret{ - { - ID: "tn9qiblgnuuut11eufquw5dev", - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}}, - }, - }, - } - - flags := newUpdateCommand(nil).Flags() - flags.Set("secret-add", "source=foo,target=foo2") - flags.Set("secret-rm", "foo") - - secrets := []*swarm.SecretReference{ - { - File: &swarm.SecretReferenceFileTarget{ - Name: "foo", - UID: "0", - GID: "0", - Mode: 292, - }, - SecretID: "tn9qiblgnuuut11eufquw5dev", - SecretName: "foo", - }, - } - - updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets) - - assert.NoError(t, err) - require.Len(t, updatedSecrets, 1) - assert.Equal(t, "tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID) - assert.Equal(t, "foo", updatedSecrets[0].SecretName) - assert.Equal(t, "foo2", updatedSecrets[0].File.Name) -} - -func TestUpdateReadOnly(t *testing.T) { - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - - // Update with --read-only=true, changed to true - flags := newUpdateCommand(nil).Flags() - flags.Set("read-only", "true") - updateService(nil, nil, flags, spec) - assert.True(t, cspec.ReadOnly) - - // Update without --read-only, no change - flags = newUpdateCommand(nil).Flags() - updateService(nil, nil, flags, spec) - assert.True(t, cspec.ReadOnly) - - // Update with --read-only=false, changed to false - flags = newUpdateCommand(nil).Flags() - flags.Set("read-only", "false") - updateService(nil, nil, flags, spec) - assert.False(t, cspec.ReadOnly) -} - -func TestUpdateStopSignal(t *testing.T) { - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - - // Update with --stop-signal=SIGUSR1 - flags := newUpdateCommand(nil).Flags() - flags.Set("stop-signal", "SIGUSR1") - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGUSR1", cspec.StopSignal) - - // Update without --stop-signal, no change - flags = newUpdateCommand(nil).Flags() - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGUSR1", cspec.StopSignal) - - // Update with --stop-signal=SIGWINCH - flags = newUpdateCommand(nil).Flags() - flags.Set("stop-signal", "SIGWINCH") - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGWINCH", cspec.StopSignal) -} diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go deleted file mode 100644 index 0cd8612b6d..0000000000 --- a/cli/command/stack/client_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package stack - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - - services []string - networks []string - secrets []string - - removedServices []string - removedNetworks []string - removedSecrets []string - - serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) - networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error) - secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error) - serviceRemoveFunc func(serviceID string) error - networkRemoveFunc func(networkID string) error - secretRemoveFunc func(secretID string) error -} - -func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { - if cli.serviceListFunc != nil { - return cli.serviceListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - servicesList := []swarm.Service{} - for _, name := range cli.services { - if belongToNamespace(name, namespace) { - servicesList = append(servicesList, serviceFromName(name)) - } - } - return servicesList, nil -} - -func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { - if cli.networkListFunc != nil { - return cli.networkListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - networksList := []types.NetworkResource{} - for _, name := range cli.networks { - if belongToNamespace(name, namespace) { - networksList = append(networksList, networkFromName(name)) - } - } - return networksList, nil -} - -func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - if cli.secretListFunc != nil { - return cli.secretListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - secretsList := []swarm.Secret{} - for _, name := range cli.secrets { - if belongToNamespace(name, namespace) { - secretsList = append(secretsList, secretFromName(name)) - } - } - return secretsList, nil -} - -func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { - if cli.serviceRemoveFunc != nil { - return cli.serviceRemoveFunc(serviceID) - } - - cli.removedServices = append(cli.removedServices, serviceID) - return nil -} - -func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error { - if cli.networkRemoveFunc != nil { - return cli.networkRemoveFunc(networkID) - } - - cli.removedNetworks = append(cli.removedNetworks, networkID) - return nil -} - -func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error { - if cli.secretRemoveFunc != nil { - return cli.secretRemoveFunc(secretID) - } - - cli.removedSecrets = append(cli.removedSecrets, secretID) - return nil -} - -func serviceFromName(name string) swarm.Service { - return swarm.Service{ - ID: "ID-" + name, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func networkFromName(name string) types.NetworkResource { - return types.NetworkResource{ - ID: "ID-" + name, - Name: name, - } -} - -func secretFromName(name string) swarm.Secret { - return swarm.Secret{ - ID: "ID-" + name, - Spec: swarm.SecretSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func namespaceFromFilters(filters filters.Args) string { - label := filters.Get("label")[0] - return strings.TrimPrefix(label, convert.LabelNamespace+"=") -} - -func belongToNamespace(id, namespace string) bool { - return strings.HasPrefix(id, namespace+"_") -} - -func objectName(namespace, name string) string { - return namespace + "_" + name -} - -func objectID(name string) string { - return "ID-" + name -} - -func buildObjectIDs(objectNames []string) []string { - IDs := make([]string, len(objectNames)) - for i, name := range objectNames { - IDs[i] = objectID(name) - } - return IDs -} diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go deleted file mode 100644 index 860bfedd1a..0000000000 --- a/cli/command/stack/cmd.go +++ /dev/null @@ -1,35 +0,0 @@ -package stack - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "stack", - Short: "Manage Docker stacks", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - cmd.AddCommand( - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newPsCommand(dockerCli), - ) - return cmd -} - -// NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := newDeployCommand(dockerCli) - // Remove the aliases at the top level - cmd.Aliases = []string{} - cmd.Tags = map[string]string{"experimental": "", "version": "1.25"} - return cmd -} diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go deleted file mode 100644 index f37d8aaa81..0000000000 --- a/cli/command/stack/common.go +++ /dev/null @@ -1,64 +0,0 @@ -package stack - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" -) - -func getStackFilter(namespace string) filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getServiceFilter(namespace string) filters.Args { - return getStackFilter(namespace) -} - -func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { - filter := opt.Value() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getAllStacksFilter() filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace) - return filter -} - -func getServices( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]swarm.Service, error) { - return apiclient.ServiceList( - ctx, - types.ServiceListOptions{Filters: getServiceFilter(namespace)}) -} - -func getStackNetworks( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]types.NetworkResource, error) { - return apiclient.NetworkList( - ctx, - types.NetworkListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackSecrets( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]swarm.Secret, error) { - return apiclient.SecretList( - ctx, - types.SecretListOptions{Filters: getStackFilter(namespace)}) -} diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go deleted file mode 100644 index 6789171702..0000000000 --- a/cli/command/stack/deploy.go +++ /dev/null @@ -1,97 +0,0 @@ -package stack - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -const ( - defaultNetworkDriver = "overlay" -) - -type deployOptions struct { - bundlefile string - composefile string - namespace string - sendRegistryAuth bool - prune bool -} - -func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts deployOptions - - cmd := &cobra.Command{ - Use: "deploy [OPTIONS] STACK", - Aliases: []string{"up"}, - Short: "Deploy a new stack or update an existing stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runDeploy(dockerCli, opts) - }, - } - - flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) - addComposefileFlag(&opts.composefile, flags) - addRegistryAuthFlag(&opts.sendRegistryAuth, flags) - flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") - flags.SetAnnotation("prune", "version", []string{"1.27"}) - return cmd -} - -func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - ctx := context.Background() - - switch { - case opts.bundlefile == "" && opts.composefile == "": - return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") - case opts.bundlefile != "" && opts.composefile != "": - return errors.Errorf("You cannot specify both a bundle file and a Compose file.") - case opts.bundlefile != "": - return deployBundle(ctx, dockerCli, opts) - default: - return deployCompose(ctx, dockerCli, opts) - } -} - -// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is -// a swarm manager. This is necessary because we must create networks before we -// create services, but the API call for creating a network does not return a -// proper status code when it can't create a network in the "global" scope. -func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error { - info, err := dockerCli.Client().Info(ctx) - if err != nil { - return err - } - if !info.Swarm.ControlAvailable { - return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - return nil -} - -// pruneServices removes services that are no longer referenced in the source -func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool { - client := dockerCli.Client() - - oldServices, err := getServices(ctx, client, namespace.Name()) - if err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err) - return true - } - - pruneServices := []swarm.Service{} - for _, service := range oldServices { - if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists { - pruneServices = append(pruneServices, service) - } - } - return removeServices(ctx, dockerCli, pruneServices) -} diff --git a/cli/command/stack/deploy_bundlefile.go b/cli/command/stack/deploy_bundlefile.go deleted file mode 100644 index 0f8f8d040b..0000000000 --- a/cli/command/stack/deploy_bundlefile.go +++ /dev/null @@ -1,91 +0,0 @@ -package stack - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" -) - -func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.namespace) - - if opts.prune { - services := map[string]struct{}{} - for service := range bundle.Services { - services[service] = struct{}{} - } - pruneServices(ctx, dockerCli, namespace, services) - } - - networks := make(map[string]types.NetworkCreate) - for _, service := range bundle.Services { - for _, networkName := range service.Networks { - networks[networkName] = types.NetworkCreate{ - Labels: convert.AddStackLabel(namespace, nil), - } - } - } - - services := make(map[string]swarm.ServiceSpec) - for internalName, service := range bundle.Services { - name := namespace.Scope(internalName) - - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - nets := []swarm.NetworkAttachmentConfig{} - for _, networkName := range service.Networks { - nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.Scope(networkName), - Aliases: []string{internalName}, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: convert.AddStackLabel(namespace, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: convert.AddStackLabel(namespace, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: nets, - } - - services[internalName] = serviceSpec - } - - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} diff --git a/cli/command/stack/deploy_composefile.go b/cli/command/stack/deploy_composefile.go deleted file mode 100644 index 700a65dce4..0000000000 --- a/cli/command/stack/deploy_composefile.go +++ /dev/null @@ -1,316 +0,0 @@ -package stack - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/cli/compose/loader" - composetypes "github.com/docker/docker/cli/compose/types" - apiclient "github.com/docker/docker/client" - dockerclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - configDetails, err := getConfigDetails(opts.composefile) - if err != nil { - return err - } - - config, err := loader.Load(configDetails) - if err != nil { - if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { - return errors.Errorf("Compose file contains unsupported options:\n\n%s\n", - propertyWarnings(fpe.Properties)) - } - - return err - } - - unsupportedProperties := loader.GetUnsupportedProperties(configDetails) - if len(unsupportedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", - strings.Join(unsupportedProperties, ", ")) - } - - deprecatedProperties := loader.GetDeprecatedProperties(configDetails) - if len(deprecatedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", - propertyWarnings(deprecatedProperties)) - } - - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.namespace) - - if opts.prune { - services := map[string]struct{}{} - for _, service := range config.Services { - services[service.Name] = struct{}{} - } - pruneServices(ctx, dockerCli, namespace, services) - } - - serviceNetworks := getServicesDeclaredNetworks(config.Services) - networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) - if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { - return err - } - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - - secrets, err := convert.Secrets(namespace, config.Secrets) - if err != nil { - return err - } - if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil { - return err - } - - services, err := convert.Services(namespace, config, dockerCli.Client()) - if err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} - -func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { - serviceNetworks := map[string]struct{}{} - for _, serviceConfig := range serviceConfigs { - if len(serviceConfig.Networks) == 0 { - serviceNetworks["default"] = struct{}{} - continue - } - for network := range serviceConfig.Networks { - serviceNetworks[network] = struct{}{} - } - } - return serviceNetworks -} - -func propertyWarnings(properties map[string]string) string { - var msgs []string - for name, description := range properties { - msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) - } - sort.Strings(msgs) - return strings.Join(msgs, "\n\n") -} - -func getConfigDetails(composefile string) (composetypes.ConfigDetails, error) { - var details composetypes.ConfigDetails - - absPath, err := filepath.Abs(composefile) - if err != nil { - return details, err - } - details.WorkingDir = filepath.Dir(absPath) - - configFile, err := getConfigFile(composefile) - if err != nil { - return details, err - } - // TODO: support multiple files - details.ConfigFiles = []composetypes.ConfigFile{*configFile} - details.Environment, err = buildEnvironment(os.Environ()) - if err != nil { - return details, err - } - return details, nil -} - -func buildEnvironment(env []string) (map[string]string, error) { - result := make(map[string]string, len(env)) - for _, s := range env { - // if value is empty, s is like "K=", not "K". - if !strings.Contains(s, "=") { - return result, errors.Errorf("unexpected environment %q", s) - } - kv := strings.SplitN(s, "=", 2) - result[kv[0]] = kv[1] - } - return result, nil -} - -func getConfigFile(filename string) (*composetypes.ConfigFile, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(bytes) - if err != nil { - return nil, err - } - return &composetypes.ConfigFile{ - Filename: filename, - Config: config, - }, nil -} - -func validateExternalNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - externalNetworks []string) error { - client := dockerCli.Client() - - for _, networkName := range externalNetworks { - network, err := client.NetworkInspect(ctx, networkName, false) - if err != nil { - if dockerclient.IsErrNetworkNotFound(err) { - return errors.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) - } - return err - } - if network.Scope != "swarm" { - return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") - } - } - - return nil -} - -func createSecrets( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - secrets []swarm.SecretSpec, -) error { - client := dockerCli.Client() - - for _, secretSpec := range secrets { - secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name) - if err == nil { - // secret already exists, then we update that - if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { - return err - } - } else if apiclient.IsErrSecretNotFound(err) { - // secret does not exist, then we create a new one. - if _, err := client.SecretCreate(ctx, secretSpec); err != nil { - return err - } - } else { - return err - } - } - return nil -} - -func createNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - networks map[string]types.NetworkCreate, -) error { - client := dockerCli.Client() - - existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]types.NetworkResource) - for _, network := range existingNetworks { - existingNetworkMap[network.Name] = network - } - - for internalName, createOpts := range networks { - name := namespace.Scope(internalName) - if _, exists := existingNetworkMap[name]; exists { - continue - } - - if createOpts.Driver == "" { - createOpts.Driver = defaultNetworkDriver - } - - fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) - if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { - return err - } - } - - return nil -} - -func deployServices( - ctx context.Context, - dockerCli *command.DockerCli, - services map[string]swarm.ServiceSpec, - namespace convert.Namespace, - sendAuth bool, -) error { - apiClient := dockerCli.Client() - out := dockerCli.Out() - - existingServices, err := getServices(ctx, apiClient, namespace.Name()) - if err != nil { - return err - } - - existingServiceMap := make(map[string]swarm.Service) - for _, service := range existingServices { - existingServiceMap[service.Spec.Name] = service - } - - for internalName, serviceSpec := range services { - name := namespace.Scope(internalName) - - encodedAuth := "" - if sendAuth { - // Retrieve encoded auth token from the image reference - image := serviceSpec.TaskTemplate.ContainerSpec.Image - encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) - if err != nil { - return err - } - } - - if service, exists := existingServiceMap[name]; exists { - fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) - - updateOpts := types.ServiceUpdateOptions{} - if sendAuth { - updateOpts.EncodedRegistryAuth = encodedAuth - } - response, err := apiClient.ServiceUpdate( - ctx, - service.ID, - service.Version, - serviceSpec, - updateOpts, - ) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - } else { - fmt.Fprintf(out, "Creating service %s\n", name) - - createOpts := types.ServiceCreateOptions{} - if sendAuth { - createOpts.EncodedRegistryAuth = encodedAuth - } - if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { - return err - } - } - } - - return nil -} diff --git a/cli/command/stack/deploy_composefile_test.go b/cli/command/stack/deploy_composefile_test.go deleted file mode 100644 index d5ef5463ff..0000000000 --- a/cli/command/stack/deploy_composefile_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package stack - -import ( - "os" - "path/filepath" - "testing" - - "github.com/docker/docker/pkg/testutil/tempfile" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetConfigDetails(t *testing.T) { - content := ` -version: "3.0" -services: - foo: - image: alpine:3.5 -` - file := tempfile.NewTempFile(t, "test-get-config-details", content) - defer file.Remove() - - details, err := getConfigDetails(file.Name()) - require.NoError(t, err) - assert.Equal(t, filepath.Dir(file.Name()), details.WorkingDir) - assert.Len(t, details.ConfigFiles, 1) - assert.Len(t, details.Environment, len(os.Environ())) -} diff --git a/cli/command/stack/deploy_test.go b/cli/command/stack/deploy_test.go deleted file mode 100644 index 817c06dd04..0000000000 --- a/cli/command/stack/deploy_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package stack - -import ( - "bytes" - "testing" - - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/cli/internal/test" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestPruneServices(t *testing.T) { - ctx := context.Background() - namespace := convert.NewNamespace("foo") - services := map[string]struct{}{ - "new": {}, - "keep": {}, - } - client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} - dockerCli := test.NewFakeCli(client, &bytes.Buffer{}) - dockerCli.SetErr(&bytes.Buffer{}) - - pruneServices(ctx, dockerCli, namespace, services) - - assert.Equal(t, buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices) -} diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go deleted file mode 100644 index 61f1e6b439..0000000000 --- a/cli/command/stack/list.go +++ /dev/null @@ -1,95 +0,0 @@ -package stack - -import ( - "sort" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - format string -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{} - - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List stacks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template") - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - stacks, err := getStacks(ctx, client) - if err != nil { - return err - } - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - stackCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewStackFormat(format), - } - sort.Sort(byName(stacks)) - return formatter.StackWrite(stackCtx, stacks) -} - -type byName []*formatter.Stack - -func (n byName) Len() int { return len(n) } -func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } -func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } - -func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) { - services, err := apiclient.ServiceList( - ctx, - types.ServiceListOptions{Filters: getAllStacksFilter()}) - if err != nil { - return nil, err - } - m := make(map[string]*formatter.Stack, 0) - for _, service := range services { - labels := service.Spec.Labels - name, ok := labels[convert.LabelNamespace] - if !ok { - return nil, errors.Errorf("cannot get label %s for service %s", - convert.LabelNamespace, service.ID) - } - ztack, ok := m[name] - if !ok { - m[name] = &formatter.Stack{ - Name: name, - Services: 1, - } - } else { - ztack.Services++ - } - } - var stacks []*formatter.Stack - for _, stack := range m { - stacks = append(stacks, stack) - } - return stacks, nil -} diff --git a/cli/command/stack/opts.go b/cli/command/stack/opts.go deleted file mode 100644 index 0d7214e962..0000000000 --- a/cli/command/stack/opts.go +++ /dev/null @@ -1,51 +0,0 @@ -package stack - -import ( - "fmt" - "io" - "os" - - "github.com/docker/docker/cli/command/bundlefile" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -func addComposefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file") - flags.SetAnnotation("compose-file", "version", []string{"1.25"}) -} - -func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") - flags.SetAnnotation("bundle-file", "experimental", nil) -} - -func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { - flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") -} - -func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { - defaultPath := fmt.Sprintf("%s.dab", namespace) - - if path == "" { - path = defaultPath - } - if _, err := os.Stat(path); err != nil { - return nil, errors.Errorf( - "Bundle %s not found. Specify the path with --file", - path) - } - - fmt.Fprintf(stderr, "Loading bundle from %s\n", path) - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer reader.Close() - - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - return nil, errors.Errorf("Error reading %s: %v\n", path, err) - } - return bundle, err -} diff --git a/cli/command/stack/ps.go b/cli/command/stack/ps.go deleted file mode 100644 index bac5307bd1..0000000000 --- a/cli/command/stack/ps.go +++ /dev/null @@ -1,76 +0,0 @@ -package stack - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type psOptions struct { - filter opts.FilterOpt - noTrunc bool - namespace string - noResolve bool - quiet bool - format string -} - -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] STACK", - Short: "List the tasks in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runPS(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - - return cmd -} - -func runPS(dockerCli *command.DockerCli, opts psOptions) error { - namespace := opts.namespace - client := dockerCli.Client() - ctx := context.Background() - - filter := getStackFilterFromOpt(opts.namespace, opts.filter) - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - return err - } - - if len(tasks) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) - return nil - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) -} diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go deleted file mode 100644 index 7df4e4c0ed..0000000000 --- a/cli/command/stack/remove.go +++ /dev/null @@ -1,121 +0,0 @@ -package stack - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type removeOptions struct { - namespaces []string -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm STACK [STACK...]", - Aliases: []string{"remove", "down"}, - Short: "Remove one or more stacks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespaces = args - return runRemove(dockerCli, opts) - }, - } - return cmd -} - -func runRemove(dockerCli command.Cli, opts removeOptions) error { - namespaces := opts.namespaces - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - for _, namespace := range namespaces { - services, err := getServices(ctx, client, namespace) - if err != nil { - return err - } - - networks, err := getStackNetworks(ctx, client, namespace) - if err != nil { - return err - } - - secrets, err := getStackSecrets(ctx, client, namespace) - if err != nil { - return err - } - - if len(services)+len(networks)+len(secrets) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) - continue - } - - hasError := removeServices(ctx, dockerCli, services) - hasError = removeSecrets(ctx, dockerCli, secrets) || hasError - hasError = removeNetworks(ctx, dockerCli, networks) || hasError - - if hasError { - errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace)) - } - } - - if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) - } - return nil -} - -func removeServices( - ctx context.Context, - dockerCli command.Cli, - services []swarm.Service, -) bool { - var err error - for _, service := range services { - fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name) - if err = dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err) - } - } - return err != nil -} - -func removeNetworks( - ctx context.Context, - dockerCli command.Cli, - networks []types.NetworkResource, -) bool { - var err error - for _, network := range networks { - fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name) - if err = dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err) - } - } - return err != nil -} - -func removeSecrets( - ctx context.Context, - dockerCli command.Cli, - secrets []swarm.Secret, -) bool { - var err error - for _, secret := range secrets { - fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name) - if err = dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err) - } - } - return err != nil -} diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go deleted file mode 100644 index 17a334db1e..0000000000 --- a/cli/command/stack/remove_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package stack - -import ( - "bytes" - "errors" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/stretchr/testify/assert" -) - -func TestRemoveStack(t *testing.T) { - allServices := []string{ - objectName("foo", "service1"), - objectName("foo", "service2"), - objectName("bar", "service1"), - objectName("bar", "service2"), - } - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{ - objectName("foo", "network1"), - objectName("bar", "network1"), - } - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{ - objectName("foo", "secret1"), - objectName("foo", "secret2"), - objectName("bar", "secret1"), - } - allSecretIDs := buildObjectIDs(allSecrets) - - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NoError(t, cmd.Execute()) - assert.Equal(t, allServiceIDs, cli.removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} - -func TestSkipEmptyStack(t *testing.T) { - buf := new(bytes.Buffer) - allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")} - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{objectName("bar", "network1")} - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{objectName("bar", "secret1")} - allSecretIDs := buildObjectIDs(allSecrets) - - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, buf)) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "Nothing found in stack: foo") - assert.Equal(t, allServiceIDs, cli.removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} - -func TestContinueAfterError(t *testing.T) { - allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")} - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")} - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")} - allSecretIDs := buildObjectIDs(allSecrets) - - removedServices := []string{} - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - - serviceRemoveFunc: func(serviceID string) error { - removedServices = append(removedServices, serviceID) - - if strings.Contains(serviceID, "foo") { - return errors.New("") - } - return nil - }, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.EqualError(t, cmd.Execute(), "Failed to remove some resources from stack: foo") - assert.Equal(t, allServiceIDs, removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go deleted file mode 100644 index 78ddd399ce..0000000000 --- a/cli/command/stack/services.go +++ /dev/null @@ -1,97 +0,0 @@ -package stack - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/service" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type servicesOptions struct { - quiet bool - format string - filter opts.FilterOpt - namespace string -} - -func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := servicesOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "services [OPTIONS] STACK", - Short: "List the services in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runServices(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - filter := getStackFilterFromOpt(opts.namespace, opts.filter) - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) - if err != nil { - return err - } - - out := dockerCli.Out() - - // if no services in this stack, print message and exit 0 - if len(services) == 0 { - fmt.Fprintf(out, "Nothing found in stack: %s\n", opts.namespace) - return nil - } - - info := map[string]formatter.ServiceListInfo{} - if !opts.quiet { - taskFilter := filters.NewArgs() - for _, service := range services { - taskFilter.Add("service", service.ID) - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - if err != nil { - return err - } - - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return err - } - - info = service.GetServicesStatus(services, nodes, tasks) - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ServicesFormat - } else { - format = formatter.TableFormatKey - } - } - - servicesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, opts.quiet), - } - return formatter.ServiceListWrite(servicesCtx, services, info) -} diff --git a/cli/command/stream.go b/cli/command/stream.go deleted file mode 100644 index 71a43fa2e9..0000000000 --- a/cli/command/stream.go +++ /dev/null @@ -1,34 +0,0 @@ -package command - -import ( - "github.com/docker/docker/pkg/term" -) - -// CommonStream is an input stream used by the DockerCli to read user input -type CommonStream struct { - fd uintptr - isTerminal bool - state *term.State -} - -// FD returns the file descriptor number for this stream -func (s *CommonStream) FD() uintptr { - return s.fd -} - -// IsTerminal returns true if this stream is connected to a terminal -func (s *CommonStream) IsTerminal() bool { - return s.isTerminal -} - -// RestoreTerminal restores normal mode to the terminal -func (s *CommonStream) RestoreTerminal() { - if s.state != nil { - term.RestoreTerminal(s.fd, s.state) - } -} - -// SetIsTerminal sets the boolean used for isTerminal -func (s *CommonStream) SetIsTerminal(isTerminal bool) { - s.isTerminal = isTerminal -} diff --git a/cli/command/swarm/client_test.go b/cli/command/swarm/client_test.go deleted file mode 100644 index 1d42b9499c..0000000000 --- a/cli/command/swarm/client_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package swarm - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - infoFunc func() (types.Info, error) - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - swarmJoinFunc func() error - swarmLeaveFunc func() error - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmUnlockFunc func(req swarm.UnlockRequest) error -} - -func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc() - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { - if cli.swarmInitFunc != nil { - return cli.swarmInitFunc() - } - return "", nil -} - -func (cli *fakeClient) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { - if cli.swarmInspectFunc != nil { - return cli.swarmInspectFunc() - } - return swarm.Swarm{}, nil -} - -func (cli *fakeClient) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { - if cli.swarmGetUnlockKeyFunc != nil { - return cli.swarmGetUnlockKeyFunc() - } - return types.SwarmUnlockKeyResponse{}, nil -} - -func (cli *fakeClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { - if cli.swarmJoinFunc != nil { - return cli.swarmJoinFunc() - } - return nil -} - -func (cli *fakeClient) SwarmLeave(ctx context.Context, force bool) error { - if cli.swarmLeaveFunc != nil { - return cli.swarmLeaveFunc() - } - return nil -} - -func (cli *fakeClient) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { - if cli.swarmUpdateFunc != nil { - return cli.swarmUpdateFunc(swarm, flags) - } - return nil -} - -func (cli *fakeClient) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { - if cli.swarmUnlockFunc != nil { - return cli.swarmUnlockFunc(req) - } - return nil -} diff --git a/cli/command/swarm/cmd.go b/cli/command/swarm/cmd.go deleted file mode 100644 index 659dbcdf7b..0000000000 --- a/cli/command/swarm/cmd.go +++ /dev/null @@ -1,29 +0,0 @@ -package swarm - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSwarmCommand returns a cobra command for `swarm` subcommands -func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "swarm", - Short: "Manage Swarm", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newInitCommand(dockerCli), - newJoinCommand(dockerCli), - newJoinTokenCommand(dockerCli), - newUnlockKeyCommand(dockerCli), - newUpdateCommand(dockerCli), - newLeaveCommand(dockerCli), - newUnlockCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go deleted file mode 100644 index 5d42b0174c..0000000000 --- a/cli/command/swarm/init.go +++ /dev/null @@ -1,99 +0,0 @@ -package swarm - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type initOptions struct { - swarmOptions - listenAddr NodeAddrOption - // Not a NodeAddrOption because it has no default port. - advertiseAddr string - dataPathAddr string - forceNewCluster bool - availability string -} - -func newInitCommand(dockerCli command.Cli) *cobra.Command { - opts := initOptions{ - listenAddr: NewListenAddrOption(), - } - - cmd := &cobra.Command{ - Use: "init [OPTIONS]", - Short: "Initialize a swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runInit(dockerCli, cmd.Flags(), opts) - }, - } - - flags := cmd.Flags() - flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") - flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") - flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") - flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") - flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) - addSwarmFlags(flags, &opts.swarmOptions) - return cmd -} - -func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - req := swarm.InitRequest{ - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - DataPathAddr: opts.dataPathAddr, - ForceNewCluster: opts.forceNewCluster, - Spec: opts.swarmOptions.ToSpec(flags), - AutoLockManagers: opts.swarmOptions.autolock, - } - if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { - case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability - default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - } - - nodeID, err := client.SwarmInit(ctx, req) - if err != nil { - if strings.Contains(err.Error(), "could not choose an IP address to advertise") || strings.Contains(err.Error(), "could not find the system's IP address") { - return errors.New(err.Error() + " - specify one with --advertise-addr") - } - return err - } - - fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { - return err - } - - fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") - - if req.AutoLockManagers { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - } - - return nil -} diff --git a/cli/command/swarm/init_test.go b/cli/command/swarm/init_test.go deleted file mode 100644 index 39cb73888c..0000000000 --- a/cli/command/swarm/init_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmInitErrorOnAPIFailure(t *testing.T) { - testCases := []struct { - name string - flags map[string]string - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - expectedError string - }{ - { - name: "init-failed", - swarmInitFunc: func() (string, error) { - return "", errors.Errorf("error initializing the swarm") - }, - expectedError: "error initializing the swarm", - }, - { - name: "init-failed-with-ip-choice", - swarmInitFunc: func() (string, error) { - return "", errors.Errorf("could not choose an IP address to advertise") - }, - expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", - }, - { - name: "swarm-inspect-after-init-failed", - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "node-inspect-after-init-failed", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - name: "swarm-get-unlock-key-after-init-failed", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting swarm unlock key") - }, - expectedError: "could not fetch unlock key: error getting swarm unlock key", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInitCommand( - test.NewFakeCli(&fakeClient{ - swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmInit(t *testing.T) { - testCases := []struct { - name string - flags map[string]string - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "init", - swarmInitFunc: func() (string, error) { - return "nodeID", nil - }, - }, - { - name: "init-autolock", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmInitFunc: func() (string, error) { - return "nodeID", nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInitCommand( - test.NewFakeCli(&fakeClient{ - swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("init-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go deleted file mode 100644 index 34730ab8c7..0000000000 --- a/cli/command/swarm/join.go +++ /dev/null @@ -1,88 +0,0 @@ -package swarm - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type joinOptions struct { - remote string - listenAddr NodeAddrOption - // Not a NodeAddrOption because it has no default port. - advertiseAddr string - dataPathAddr string - token string - availability string -} - -func newJoinCommand(dockerCli command.Cli) *cobra.Command { - opts := joinOptions{ - listenAddr: NewListenAddrOption(), - } - - cmd := &cobra.Command{ - Use: "join [OPTIONS] HOST:PORT", - Short: "Join a swarm as a node and/or manager", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.remote = args[0] - return runJoin(dockerCli, cmd.Flags(), opts) - }, - } - - flags := cmd.Flags() - flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") - flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") - flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") - flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) - return cmd -} - -func runJoin(dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - req := swarm.JoinRequest{ - JoinToken: opts.token, - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - DataPathAddr: opts.dataPathAddr, - RemoteAddrs: []string{opts.remote}, - } - if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { - case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability - default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - } - - err := client.SwarmJoin(ctx, req) - if err != nil { - return err - } - - info, err := client.Info(ctx) - if err != nil { - return err - } - - if info.Swarm.ControlAvailable { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") - } else { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") - } - return nil -} diff --git a/cli/command/swarm/join_test.go b/cli/command/swarm/join_test.go deleted file mode 100644 index 6893f68e1d..0000000000 --- a/cli/command/swarm/join_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmJoinErrors(t *testing.T) { - testCases := []struct { - name string - args []string - swarmJoinFunc func() error - infoFunc func() (types.Info, error) - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires exactly 1 argument", - }, - { - name: "too-many-args", - args: []string{"remote1", "remote2"}, - expectedError: "requires exactly 1 argument", - }, - { - name: "join-failed", - args: []string{"remote"}, - swarmJoinFunc: func() error { - return errors.Errorf("error joining the swarm") - }, - expectedError: "error joining the swarm", - }, - { - name: "join-failed-on-init", - args: []string{"remote"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinCommand( - test.NewFakeCli(&fakeClient{ - swarmJoinFunc: tc.swarmJoinFunc, - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmJoin(t *testing.T) { - testCases := []struct { - name string - infoFunc func() (types.Info, error) - expected string - }{ - { - name: "join-as-manager", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - ControlAvailable: true, - }, - }, nil - }, - expected: "This node joined a swarm as a manager.", - }, - { - name: "join-as-worker", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - ControlAvailable: false, - }, - }, nil - }, - expected: "This node joined a swarm as a worker.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs([]string{"remote"}) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, strings.TrimSpace(buf.String()), tc.expected) - } -} diff --git a/cli/command/swarm/join_token.go b/cli/command/swarm/join_token.go deleted file mode 100644 index 465f379c2d..0000000000 --- a/cli/command/swarm/join_token.go +++ /dev/null @@ -1,119 +0,0 @@ -package swarm - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type joinTokenOptions struct { - role string - rotate bool - quiet bool -} - -func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { - opts := joinTokenOptions{} - - cmd := &cobra.Command{ - Use: "join-token [OPTIONS] (worker|manager)", - Short: "Manage join tokens", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.role = args[0] - return runJoinToken(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate join token") - flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") - - return cmd -} - -func runJoinToken(dockerCli command.Cli, opts joinTokenOptions) error { - worker := opts.role == "worker" - manager := opts.role == "manager" - - if !worker && !manager { - return errors.New("unknown role " + opts.role) - } - - client := dockerCli.Client() - ctx := context.Background() - - if opts.rotate { - flags := swarm.UpdateFlags{ - RotateWorkerToken: worker, - RotateManagerToken: manager, - } - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { - return err - } - - if !opts.quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated %s join token.\n\n", opts.role) - } - } - - // second SwarmInspect in this function, - // this is necessary since SwarmUpdate after first changes the join tokens - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if opts.quiet && worker { - fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Worker) - return nil - } - - if opts.quiet && manager { - fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Manager) - return nil - } - - info, err := client.Info(ctx) - if err != nil { - return err - } - - return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) -} - -func printJoinCommand(ctx context.Context, dockerCli command.Cli, nodeID string, worker bool, manager bool) error { - client := dockerCli.Client() - - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) - if err != nil { - return err - } - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if node.ManagerStatus != nil { - if worker { - fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) - } - if manager { - fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) - } - } - - return nil -} diff --git a/cli/command/swarm/join_token_test.go b/cli/command/swarm/join_token_test.go deleted file mode 100644 index 9289189009..0000000000 --- a/cli/command/swarm/join_token_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmJoinTokenErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - nodeInspectFunc func() (swarm.Node, []byte, error) - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires exactly 1 argument", - }, - { - name: "too-many-args", - args: []string{"worker", "manager"}, - expectedError: "requires exactly 1 argument", - }, - { - name: "invalid-args", - args: []string{"foo"}, - expectedError: "unknown role foo", - }, - { - name: "swarm-inspect-failed", - args: []string{"worker"}, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-inspect-rotate-failed", - args: []string{"worker"}, - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-update-failed", - args: []string{"worker"}, - flags: map[string]string{ - flagRotate: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "node-inspect-failed", - args: []string{"worker"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - expectedError: "error inspecting node", - }, - { - name: "info-failed", - args: []string{"worker"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinTokenCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmJoinToken(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "worker", - args: []string{"worker"}, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager", - args: []string{"manager"}, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager-rotate", - args: []string{"manager"}, - flags: map[string]string{ - flagRotate: "true", - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "worker-quiet", - args: []string{"worker"}, - flags: map[string]string{ - flagQuiet: "true", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager-quiet", - args: []string{"manager"}, - flags: map[string]string{ - flagQuiet: "true", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinTokenCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("jointoken-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/leave.go b/cli/command/swarm/leave.go deleted file mode 100644 index 128ed46d8a..0000000000 --- a/cli/command/swarm/leave.go +++ /dev/null @@ -1,44 +0,0 @@ -package swarm - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type leaveOptions struct { - force bool -} - -func newLeaveCommand(dockerCli command.Cli) *cobra.Command { - opts := leaveOptions{} - - cmd := &cobra.Command{ - Use: "leave [OPTIONS]", - Short: "Leave the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runLeave(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force this node to leave the swarm, ignoring warnings") - return cmd -} - -func runLeave(dockerCli command.Cli, opts leaveOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if err := client.SwarmLeave(ctx, opts.force); err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), "Node left the swarm.") - return nil -} diff --git a/cli/command/swarm/leave_test.go b/cli/command/swarm/leave_test.go deleted file mode 100644 index 030f18039a..0000000000 --- a/cli/command/swarm/leave_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmLeaveErrors(t *testing.T) { - testCases := []struct { - name string - args []string - swarmLeaveFunc func() error - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "leave-failed", - swarmLeaveFunc: func() error { - return errors.Errorf("error leaving the swarm") - }, - expectedError: "error leaving the swarm", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newLeaveCommand( - test.NewFakeCli(&fakeClient{ - swarmLeaveFunc: tc.swarmLeaveFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmLeave(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newLeaveCommand( - test.NewFakeCli(&fakeClient{}, buf)) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, "Node left the swarm.", strings.TrimSpace(buf.String())) -} diff --git a/cli/command/swarm/opts.go b/cli/command/swarm/opts.go deleted file mode 100644 index d53498586a..0000000000 --- a/cli/command/swarm/opts.go +++ /dev/null @@ -1,224 +0,0 @@ -package swarm - -import ( - "encoding/csv" - "encoding/pem" - "fmt" - "io/ioutil" - "strings" - "time" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -const ( - defaultListenAddr = "0.0.0.0:2377" - - flagCertExpiry = "cert-expiry" - flagDispatcherHeartbeat = "dispatcher-heartbeat" - flagListenAddr = "listen-addr" - flagAdvertiseAddr = "advertise-addr" - flagDataPathAddr = "data-path-addr" - flagQuiet = "quiet" - flagRotate = "rotate" - flagToken = "token" - flagTaskHistoryLimit = "task-history-limit" - flagExternalCA = "external-ca" - flagMaxSnapshots = "max-snapshots" - flagSnapshotInterval = "snapshot-interval" - flagLockKey = "lock-key" - flagAutolock = "autolock" - flagAvailability = "availability" -) - -type swarmOptions struct { - taskHistoryLimit int64 - dispatcherHeartbeat time.Duration - nodeCertExpiry time.Duration - externalCA ExternalCAOption - maxSnapshots uint64 - snapshotInterval uint64 - autolock bool -} - -// NodeAddrOption is a pflag.Value for listening addresses -type NodeAddrOption struct { - addr string -} - -// String prints the representation of this flag -func (a *NodeAddrOption) String() string { - return a.Value() -} - -// Set the value for this flag -func (a *NodeAddrOption) Set(value string) error { - addr, err := opts.ParseTCPAddr(value, a.addr) - if err != nil { - return err - } - a.addr = addr - return nil -} - -// Type returns the type of this flag -func (a *NodeAddrOption) Type() string { - return "node-addr" -} - -// Value returns the value of this option as addr:port -func (a *NodeAddrOption) Value() string { - return strings.TrimPrefix(a.addr, "tcp://") -} - -// NewNodeAddrOption returns a new node address option -func NewNodeAddrOption(addr string) NodeAddrOption { - return NodeAddrOption{addr} -} - -// NewListenAddrOption returns a NodeAddrOption with default values -func NewListenAddrOption() NodeAddrOption { - return NewNodeAddrOption(defaultListenAddr) -} - -// ExternalCAOption is a Value type for parsing external CA specifications. -type ExternalCAOption struct { - values []*swarm.ExternalCA -} - -// Set parses an external CA option. -func (m *ExternalCAOption) Set(value string) error { - parsed, err := parseExternalCA(value) - if err != nil { - return err - } - - m.values = append(m.values, parsed) - return nil -} - -// Type returns the type of this option. -func (m *ExternalCAOption) Type() string { - return "external-ca" -} - -// String returns a string repr of this option. -func (m *ExternalCAOption) String() string { - externalCAs := []string{} - for _, externalCA := range m.values { - repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL) - externalCAs = append(externalCAs, repr) - } - return strings.Join(externalCAs, ", ") -} - -// Value returns the external CAs -func (m *ExternalCAOption) Value() []*swarm.ExternalCA { - return m.values -} - -// parseExternalCA parses an external CA specification from the command line, -// such as protocol=cfssl,url=https://example.com. -func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { - csvReader := csv.NewReader(strings.NewReader(caSpec)) - fields, err := csvReader.Read() - if err != nil { - return nil, err - } - - externalCA := swarm.ExternalCA{ - Options: make(map[string]string), - } - - var ( - hasProtocol bool - hasURL bool - ) - - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - - if len(parts) != 2 { - return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) - } - - key, value := parts[0], parts[1] - - switch strings.ToLower(key) { - case "protocol": - hasProtocol = true - if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { - externalCA.Protocol = swarm.ExternalCAProtocolCFSSL - } else { - return nil, errors.Errorf("unrecognized external CA protocol %s", value) - } - case "url": - hasURL = true - externalCA.URL = value - case "cacert": - cacontents, err := ioutil.ReadFile(value) - if err != nil { - return nil, errors.Wrap(err, "unable to read CA cert for external CA") - } - if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { - return nil, errors.New("CA cert for external CA must be in PEM format") - } - externalCA.CACert = string(cacontents) - default: - externalCA.Options[key] = value - } - } - - if !hasProtocol { - return nil, errors.New("the external-ca option needs a protocol= parameter") - } - if !hasURL { - return nil, errors.New("the external-ca option needs a url= parameter") - } - - return &externalCA, nil -} - -func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { - flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") - flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h)") - flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)") - flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") - flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") - flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) - flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") - flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) -} - -func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { - if flags.Changed(flagTaskHistoryLimit) { - spec.Orchestration.TaskHistoryRetentionLimit = &opts.taskHistoryLimit - } - if flags.Changed(flagDispatcherHeartbeat) { - spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat - } - if flags.Changed(flagCertExpiry) { - spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry - } - if flags.Changed(flagExternalCA) { - spec.CAConfig.ExternalCAs = opts.externalCA.Value() - } - if flags.Changed(flagMaxSnapshots) { - spec.Raft.KeepOldSnapshots = &opts.maxSnapshots - } - if flags.Changed(flagSnapshotInterval) { - spec.Raft.SnapshotInterval = opts.snapshotInterval - } - if flags.Changed(flagAutolock) { - spec.EncryptionConfig.AutoLockManagers = opts.autolock - } -} - -func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { - var spec swarm.Spec - opts.mergeSwarmSpec(&spec, flags) - return spec -} diff --git a/cli/command/swarm/opts_test.go b/cli/command/swarm/opts_test.go deleted file mode 100644 index c694cc1bdd..0000000000 --- a/cli/command/swarm/opts_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package swarm - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNodeAddrOptionSetHostAndPort(t *testing.T) { - opt := NewNodeAddrOption("old:123") - addr := "newhost:5555" - assert.NoError(t, opt.Set(addr)) - assert.Equal(t, addr, opt.Value()) -} - -func TestNodeAddrOptionSetHostOnly(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set("newhost")) - assert.Equal(t, "newhost:2377", opt.Value()) -} - -func TestNodeAddrOptionSetHostOnlyIPv6(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set("::1")) - assert.Equal(t, "[::1]:2377", opt.Value()) -} - -func TestNodeAddrOptionSetPortOnly(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set(":4545")) - assert.Equal(t, "0.0.0.0:4545", opt.Value()) -} - -func TestNodeAddrOptionSetInvalidFormat(t *testing.T) { - opt := NewListenAddrOption() - assert.EqualError(t, opt.Set("http://localhost:4545"), "Invalid proto, expected tcp: http://localhost:4545") -} - -func TestExternalCAOptionErrors(t *testing.T) { - testCases := []struct { - externalCA string - expectedError string - }{ - { - externalCA: "", - expectedError: "EOF", - }, - { - externalCA: "anything", - expectedError: "invalid field 'anything' must be a key=value pair", - }, - { - externalCA: "foo=bar", - expectedError: "the external-ca option needs a protocol= parameter", - }, - { - externalCA: "protocol=baz", - expectedError: "unrecognized external CA protocol baz", - }, - { - externalCA: "protocol=cfssl", - expectedError: "the external-ca option needs a url= parameter", - }, - } - for _, tc := range testCases { - opt := &ExternalCAOption{} - assert.EqualError(t, opt.Set(tc.externalCA), tc.expectedError) - } -} - -func TestExternalCAOption(t *testing.T) { - testCases := []struct { - externalCA string - expected string - }{ - { - externalCA: "protocol=cfssl,url=anything", - expected: "cfssl: anything", - }, - { - externalCA: "protocol=CFSSL,url=anything", - expected: "cfssl: anything", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com", - expected: "cfssl: https://example.com", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com,foo=bar", - expected: "cfssl: https://example.com", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com,foo=bar,foo=baz", - expected: "cfssl: https://example.com", - }, - } - for _, tc := range testCases { - opt := &ExternalCAOption{} - assert.NoError(t, opt.Set(tc.externalCA)) - assert.Equal(t, tc.expected, opt.String()) - } -} - -func TestExternalCAOptionMultiple(t *testing.T) { - opt := &ExternalCAOption{} - assert.NoError(t, opt.Set("protocol=cfssl,url=https://example.com")) - assert.NoError(t, opt.Set("protocol=CFSSL,url=anything")) - assert.Len(t, opt.Value(), 2) - assert.Equal(t, "cfssl: https://example.com, cfssl: anything", opt.String()) -} diff --git a/cli/command/swarm/testdata/init-init-autolock.golden b/cli/command/swarm/testdata/init-init-autolock.golden deleted file mode 100644 index cdd3c666b6..0000000000 --- a/cli/command/swarm/testdata/init-init-autolock.golden +++ /dev/null @@ -1,11 +0,0 @@ -Swarm initialized: current node (nodeID) is now a manager. - -To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. - -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/init-init.golden b/cli/command/swarm/testdata/init-init.golden deleted file mode 100644 index 6e82be010e..0000000000 --- a/cli/command/swarm/testdata/init-init.golden +++ /dev/null @@ -1,4 +0,0 @@ -Swarm initialized: current node (nodeID) is now a manager. - -To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. - diff --git a/cli/command/swarm/testdata/jointoken-manager-quiet.golden b/cli/command/swarm/testdata/jointoken-manager-quiet.golden deleted file mode 100644 index 0c7cfc6088..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -manager-join-token diff --git a/cli/command/swarm/testdata/jointoken-manager-rotate.golden b/cli/command/swarm/testdata/jointoken-manager-rotate.golden deleted file mode 100644 index b4d0a48f66..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager-rotate.golden +++ /dev/null @@ -1,5 +0,0 @@ -Successfully rotated manager join token. - -To add a manager to this swarm, run the following command: - - docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-manager.golden b/cli/command/swarm/testdata/jointoken-manager.golden deleted file mode 100644 index 522b2968fe..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager.golden +++ /dev/null @@ -1,3 +0,0 @@ -To add a manager to this swarm, run the following command: - - docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-worker-quiet.golden b/cli/command/swarm/testdata/jointoken-worker-quiet.golden deleted file mode 100644 index b445e191e5..0000000000 --- a/cli/command/swarm/testdata/jointoken-worker-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -worker-join-token diff --git a/cli/command/swarm/testdata/jointoken-worker.golden b/cli/command/swarm/testdata/jointoken-worker.golden deleted file mode 100644 index 899df703fd..0000000000 --- a/cli/command/swarm/testdata/jointoken-worker.golden +++ /dev/null @@ -1,3 +0,0 @@ -To add a worker to this swarm, run the following command: - - docker swarm join --token worker-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden deleted file mode 100644 index ed53505e25..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden deleted file mode 100644 index ed53505e25..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden deleted file mode 100644 index 89152b8643..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden +++ /dev/null @@ -1,9 +0,0 @@ -Successfully rotated manager unlock key. - -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key.golden deleted file mode 100644 index 8316df478c..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key.golden +++ /dev/null @@ -1,7 +0,0 @@ -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-all-flags-quiet.golden b/cli/command/swarm/testdata/update-all-flags-quiet.golden deleted file mode 100644 index 3d195a2586..0000000000 --- a/cli/command/swarm/testdata/update-all-flags-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -Swarm updated. diff --git a/cli/command/swarm/testdata/update-autolock-unlock-key.golden b/cli/command/swarm/testdata/update-autolock-unlock-key.golden deleted file mode 100644 index a077b9e167..0000000000 --- a/cli/command/swarm/testdata/update-autolock-unlock-key.golden +++ /dev/null @@ -1,8 +0,0 @@ -Swarm updated. -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-noargs.golden b/cli/command/swarm/testdata/update-noargs.golden deleted file mode 100644 index 381c0ccf1f..0000000000 --- a/cli/command/swarm/testdata/update-noargs.golden +++ /dev/null @@ -1,13 +0,0 @@ -Update the swarm - -Usage: - update [OPTIONS] [flags] - -Flags: - --autolock Change manager autolocking setting (true|false) - --cert-expiry duration Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s) - --dispatcher-heartbeat duration Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s) - --external-ca external-ca Specifications of one or more certificate signing endpoints - --max-snapshots uint Number of additional Raft snapshots to retain - --snapshot-interval uint Number of log entries between Raft snapshots (default 10000) - --task-history-limit int Task history retention limit (default 5) diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go deleted file mode 100644 index c1d9b99189..0000000000 --- a/cli/command/swarm/unlock.go +++ /dev/null @@ -1,78 +0,0 @@ -package swarm - -import ( - "bufio" - "fmt" - "io" - "strings" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/net/context" -) - -type unlockOptions struct{} - -func newUnlockCommand(dockerCli command.Cli) *cobra.Command { - opts := unlockOptions{} - - cmd := &cobra.Command{ - Use: "unlock", - Short: "Unlock swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUnlock(dockerCli, opts) - }, - } - - return cmd -} - -func runUnlock(dockerCli command.Cli, opts unlockOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - // First see if the node is actually part of a swarm, and if it is actually locked first. - // If it's in any other state than locked, don't ask for the key. - info, err := client.Info(ctx) - if err != nil { - return err - } - - switch info.Swarm.LocalNodeState { - case swarm.LocalNodeStateInactive: - return errors.New("Error: This node is not part of a swarm") - case swarm.LocalNodeStateLocked: - break - default: - return errors.New("Error: swarm is not locked") - } - - key, err := readKey(dockerCli.In(), "Please enter unlock key: ") - if err != nil { - return err - } - req := swarm.UnlockRequest{ - UnlockKey: key, - } - - return client.SwarmUnlock(ctx, req) -} - -func readKey(in *command.InStream, prompt string) (string, error) { - if in.IsTerminal() { - fmt.Print(prompt) - dt, err := terminal.ReadPassword(int(in.FD())) - fmt.Println() - return string(dt), err - } - key, err := bufio.NewReader(in).ReadString('\n') - if err == io.EOF { - err = nil - } - return strings.TrimSpace(key), err -} diff --git a/cli/command/swarm/unlock_key.go b/cli/command/swarm/unlock_key.go deleted file mode 100644 index 77c97d88ea..0000000000 --- a/cli/command/swarm/unlock_key.go +++ /dev/null @@ -1,86 +0,0 @@ -package swarm - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type unlockKeyOptions struct { - rotate bool - quiet bool -} - -func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { - opts := unlockKeyOptions{} - - cmd := &cobra.Command{ - Use: "unlock-key [OPTIONS]", - Short: "Manage the unlock key", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUnlockKey(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate unlock key") - flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") - - return cmd -} - -func runUnlockKey(dockerCli command.Cli, opts unlockKeyOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.rotate { - flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if !sw.Spec.EncryptionConfig.AutoLockManagers { - return errors.New("cannot rotate because autolock is not turned on") - } - - if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { - return err - } - - if !opts.quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated manager unlock key.\n\n") - } - } - - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - - if unlockKeyResp.UnlockKey == "" { - return errors.New("no unlock key is set") - } - - if opts.quiet { - fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) - return nil - } - - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - return nil -} - -func printUnlockCommand(ctx context.Context, dockerCli command.Cli, unlockKey string) { - if len(unlockKey) > 0 { - fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) - } - return -} diff --git a/cli/command/swarm/unlock_key_test.go b/cli/command/swarm/unlock_key_test.go deleted file mode 100644 index 23752104aa..0000000000 --- a/cli/command/swarm/unlock_key_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUnlockKeyErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "swarm-inspect-rotate-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-rotate-no-autolock-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - expectedError: "cannot rotate because autolock is not turned on", - }, - { - name: "swarm-update-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "swarm-get-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") - }, - expectedError: "error getting unlock key", - }, - { - name: "swarm-no-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "", - }, nil - }, - expectedError: "no unlock key is set", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockKeyCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUnlockKey(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - }{ - { - name: "unlock-key", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-quiet", - flags: map[string]string{ - flagQuiet: "true", - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-rotate", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-rotate-quiet", - flags: map[string]string{ - flagQuiet: "true", - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockKeyCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go deleted file mode 100644 index 056c9746c6..0000000000 --- a/cli/command/swarm/unlock_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUnlockErrors(t *testing.T) { - testCases := []struct { - name string - args []string - input string - swarmUnlockFunc func(req swarm.UnlockRequest) error - infoFunc func() (types.Info, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "is-not-part-of-a-swarm", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateInactive, - }, - }, nil - }, - expectedError: "This node is not part of a swarm", - }, - { - name: "is-not-locked", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateActive, - }, - }, nil - }, - expectedError: "Error: swarm is not locked", - }, - { - name: "unlockrequest-failed", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateLocked, - }, - }, nil - }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - return errors.Errorf("error unlocking the swarm") - }, - expectedError: "error unlocking the swarm", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - swarmUnlockFunc: tc.swarmUnlockFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUnlock(t *testing.T) { - input := "unlockKey" - buf := new(bytes.Buffer) - dockerCli := test.NewFakeCli(&fakeClient{ - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateLocked, - }, - }, nil - }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - if req.UnlockKey != input { - return errors.Errorf("Invalid unlock key") - } - return nil - }, - }, buf) - dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) - cmd := newUnlockCommand(dockerCli) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/swarm/update.go b/cli/command/swarm/update.go deleted file mode 100644 index 1ccd268e74..0000000000 --- a/cli/command/swarm/update.go +++ /dev/null @@ -1,72 +0,0 @@ -package swarm - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { - opts := swarmOptions{} - - cmd := &cobra.Command{ - Use: "update [OPTIONS]", - Short: "Update the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), opts) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if cmd.Flags().NFlag() == 0 { - return pflag.ErrHelp - } - return nil - }, - } - - cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") - addSwarmFlags(cmd.Flags(), &opts) - return cmd -} - -func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var updateFlags swarm.UpdateFlags - - swarmInspect, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - prevAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - - opts.mergeSwarmSpec(&swarmInspect.Spec, flags) - - curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - - err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), "Swarm updated.") - - if curAutoLock && !prevAutoLock { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - } - - return nil -} diff --git a/cli/command/swarm/update_test.go b/cli/command/swarm/update_test.go deleted file mode 100644 index 65366ddd20..0000000000 --- a/cli/command/swarm/update_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUpdateErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "swarm-inspect-error", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-update-error", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "swarm-unlockkey-error", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") - }, - expectedError: "error getting unlock key", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUpdate(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - }{ - { - name: "noargs", - }, - { - name: "all-flags-quiet", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - flagDispatcherHeartbeat: "10s", - flagCertExpiry: "20s", - flagExternalCA: "protocol=cfssl,url=https://example.com.", - flagMaxSnapshots: "10", - flagSnapshotInterval: "100", - flagAutolock: "true", - flagQuiet: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.Errorf("historyLimit not correctly set") - } - heartbeatDuration, err := time.ParseDuration("10s") - if err != nil { - return err - } - if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { - return errors.Errorf("heartbeatPeriodLimit not correctly set") - } - certExpiryDuration, err := time.ParseDuration("20s") - if err != nil { - return err - } - if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { - return errors.Errorf("certExpiry not correctly set") - } - if len(swarm.CAConfig.ExternalCAs) != 1 { - return errors.Errorf("externalCA not correctly set") - } - if *swarm.Raft.KeepOldSnapshots != 10 { - return errors.Errorf("keepOldSnapshots not correctly set") - } - if swarm.Raft.SnapshotInterval != 100 { - return errors.Errorf("snapshotInterval not correctly set") - } - if !swarm.EncryptionConfig.AutoLockManagers { - return errors.Errorf("autolock not correctly set") - } - return nil - }, - }, - { - name: "autolock-unlock-key", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - flagAutolock: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.Errorf("historyLimit not correctly set") - } - return nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(buf) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("update-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go deleted file mode 100644 index ab3beb895a..0000000000 --- a/cli/command/system/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSystemCommand returns a cobra command for `system` subcommands -func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "system", - Short: "Manage Docker", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewEventsCommand(dockerCli), - NewInfoCommand(dockerCli), - NewDiskUsageCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - - return cmd -} diff --git a/cli/command/system/df.go b/cli/command/system/df.go deleted file mode 100644 index 67b3b31d87..0000000000 --- a/cli/command/system/df.go +++ /dev/null @@ -1,68 +0,0 @@ -package system - -import ( - "errors" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type diskUsageOptions struct { - verbose bool - format string -} - -// NewDiskUsageCommand creates a new cobra.Command for `docker df` -func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts diskUsageOptions - - cmd := &cobra.Command{ - Use: "df [OPTIONS]", - Short: "Show docker disk usage", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runDiskUsage(dockerCli, opts) - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Show detailed information on space usage") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - - return cmd -} - -func runDiskUsage(dockerCli *command.DockerCli, 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()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - - duCtx := formatter.DiskUsageContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewDiskUsageFormat(format), - }, - LayersSize: du.LayersSize, - Images: du.Images, - Containers: du.Containers, - Volumes: du.Volumes, - Verbose: opts.verbose, - } - - return duCtx.Write() -} diff --git a/cli/command/system/events.go b/cli/command/system/events.go deleted file mode 100644 index 441ef91d33..0000000000 --- a/cli/command/system/events.go +++ /dev/null @@ -1,140 +0,0 @@ -package system - -import ( - "fmt" - "io" - "io/ioutil" - "sort" - "strings" - "text/template" - "time" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - eventtypes "github.com/docker/docker/api/types/events" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonlog" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" -) - -type eventsOptions struct { - since string - until string - filter opts.FilterOpt - format string -} - -// NewEventsCommand creates a new cobra.Command for `docker events` -func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := eventsOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "events [OPTIONS]", - Short: "Get real time events from the server", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") - flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&opts.format, "format", "", "Format the output using the given Go template") - - return cmd -} - -func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { - tmpl, err := makeTemplate(opts.format) - if err != nil { - return cli.StatusError{ - StatusCode: 64, - Status: "Error parsing format: " + err.Error()} - } - options := types.EventsOptions{ - Since: opts.since, - Until: opts.until, - Filters: opts.filter.Value(), - } - - ctx, cancel := context.WithCancel(context.Background()) - events, errs := dockerCli.Client().Events(ctx, options) - defer cancel() - - out := dockerCli.Out() - - for { - select { - case event := <-events: - if err := handleEvent(out, event, tmpl); err != nil { - return err - } - case err := <-errs: - if err == io.EOF { - return nil - } - return err - } - } -} - -func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - if tmpl == nil { - return prettyPrintEvent(out, event) - } - - return formatEvent(out, event, tmpl) -} - -func makeTemplate(format string) (*template.Template, error) { - if format == "" { - return nil, nil - } - tmpl, err := templates.Parse(format) - if err != nil { - return tmpl, err - } - // we execute the template for an empty message, so as to validate - // a bad template like "{{.badFieldString}}" - return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{}) -} - -// prettyPrintEvent prints all types of event information. -// Each output includes the event type, actor id, name and action. -// Actor attributes are printed at the end if the actor has any. -func prettyPrintEvent(out io.Writer, event eventtypes.Message) error { - if event.TimeNano != 0 { - fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) - } else if event.Time != 0 { - fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) - } - - fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) - - if len(event.Actor.Attributes) > 0 { - var attrs []string - var keys []string - for k := range event.Actor.Attributes { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := event.Actor.Attributes[k] - attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) - } - fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) - } - fmt.Fprint(out, "\n") - return nil -} - -func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - defer out.Write([]byte{'\n'}) - return tmpl.Execute(out, event) -} diff --git a/cli/command/system/info.go b/cli/command/system/info.go deleted file mode 100644 index 8ded8124dc..0000000000 --- a/cli/command/system/info.go +++ /dev/null @@ -1,369 +0,0 @@ -package system - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/debug" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/templates" - "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type infoOptions struct { - format string -} - -// NewInfoCommand creates a new cobra.Command for `docker info` -func NewInfoCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts infoOptions - - cmd := &cobra.Command{ - Use: "info [OPTIONS]", - Short: "Display system-wide information", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runInfo(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error { - ctx := context.Background() - info, err := dockerCli.Client().Info(ctx) - if err != nil { - return err - } - if opts.format == "" { - return prettyPrintInfo(dockerCli, info) - } - return formatInfo(dockerCli, info, opts.format) -} - -func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { - fmt.Fprintf(dockerCli.Out(), "Containers: %d\n", info.Containers) - fmt.Fprintf(dockerCli.Out(), " Running: %d\n", info.ContainersRunning) - fmt.Fprintf(dockerCli.Out(), " Paused: %d\n", info.ContainersPaused) - fmt.Fprintf(dockerCli.Out(), " Stopped: %d\n", info.ContainersStopped) - fmt.Fprintf(dockerCli.Out(), "Images: %d\n", info.Images) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Server Version: %s\n", info.ServerVersion) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Storage Driver: %s\n", info.Driver) - if info.DriverStatus != nil { - for _, pair := range info.DriverStatus { - fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) - } - - } - if info.SystemStatus != nil { - for _, pair := range info.SystemStatus { - fmt.Fprintf(dockerCli.Out(), "%s: %s\n", pair[0], pair[1]) - } - } - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver) - - fmt.Fprintf(dockerCli.Out(), "Plugins: \n") - fmt.Fprintf(dockerCli.Out(), " Volume:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Volume, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - fmt.Fprintf(dockerCli.Out(), " Network:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Network, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - - if len(info.Plugins.Authorization) != 0 { - fmt.Fprintf(dockerCli.Out(), " Authorization:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - } - - fmt.Fprintf(dockerCli.Out(), " Log:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - - fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState) - if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked { - fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID) - if info.Swarm.Error != "" { - fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) - } - fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable) - if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { - fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID) - fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) - fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) - fmt.Fprintf(dockerCli.Out(), " Orchestration:\n") - taskHistoryRetentionLimit := int64(0) - if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil { - taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit - } - fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit) - fmt.Fprintf(dockerCli.Out(), " Raft:\n") - fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) - if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil { - fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots) - } - fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) - fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick) - fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n") - fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(time.Duration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))) - fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n") - fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry)) - if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 { - fmt.Fprintf(dockerCli.Out(), " External CAs:\n") - for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs { - fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL) - } - } - } - fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr) - managers := []string{} - for _, entry := range info.Swarm.RemoteManagers { - managers = append(managers, entry.Addr) - } - if len(managers) > 0 { - sort.Strings(managers) - fmt.Fprintf(dockerCli.Out(), " Manager Addresses:\n") - for _, entry := range managers { - fmt.Fprintf(dockerCli.Out(), " %s\n", entry) - } - } - } - - if len(info.Runtimes) > 0 { - fmt.Fprintf(dockerCli.Out(), "Runtimes:") - for name := range info.Runtimes { - fmt.Fprintf(dockerCli.Out(), " %s", name) - } - fmt.Fprint(dockerCli.Out(), "\n") - fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime) - } - - if info.OSType == "linux" { - fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary) - - for _, ci := range []struct { - Name string - Commit types.Commit - }{ - {"containerd", info.ContainerdCommit}, - {"runc", info.RuncCommit}, - {"init", info.InitCommit}, - } { - fmt.Fprintf(dockerCli.Out(), "%s version: %s", ci.Name, ci.Commit.ID) - if ci.Commit.ID != ci.Commit.Expected { - fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected) - } - fmt.Fprintf(dockerCli.Out(), "\n") - } - if len(info.SecurityOptions) != 0 { - kvs, err := types.DecodeSecurityOptions(info.SecurityOptions) - if err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Security Options:\n") - for _, so := range kvs { - fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name) - for _, o := range so.Options { - switch o.Key { - case "profile": - if o.Value != "default" { - fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n") - } - fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value) - } - } - } - } - } - - // Isolation only has meaning on a Windows daemon. - if info.OSType == "windows" { - fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation) - } - - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Architecture: %s\n", info.Architecture) - fmt.Fprintf(dockerCli.Out(), "CPUs: %d\n", info.NCPU) - fmt.Fprintf(dockerCli.Out(), "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID) - fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir) - fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", debug.IsEnabled()) - fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug) - - if info.Debug { - fmt.Fprintf(dockerCli.Out(), " File Descriptors: %d\n", info.NFd) - fmt.Fprintf(dockerCli.Out(), " Goroutines: %d\n", info.NGoroutines) - fmt.Fprintf(dockerCli.Out(), " System Time: %s\n", info.SystemTime) - fmt.Fprintf(dockerCli.Out(), " EventsListeners: %d\n", info.NEventsListener) - } - - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Http Proxy: %s\n", info.HTTPProxy) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Https Proxy: %s\n", info.HTTPSProxy) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "No Proxy: %s\n", info.NoProxy) - - if info.IndexServerAddress != "" { - u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username - if len(u) > 0 { - fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u) - } - fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress) - } - - if info.Labels != nil { - fmt.Fprintln(dockerCli.Out(), "Labels:") - for _, attribute := range info.Labels { - fmt.Fprintf(dockerCli.Out(), " %s\n", attribute) - } - // TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out - // after 3 release cycles (17.12). For now, a WARNING will be generated. The following will - // be removed eventually. - labelMap := map[string]string{} - for _, label := range info.Labels { - stringSlice := strings.SplitN(label, "=", 2) - if len(stringSlice) > 1 { - // If there is a conflict we will throw out a warning - if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { - fmt.Fprintln(dockerCli.Err(), "WARNING: labels with duplicate keys and conflicting values have been deprecated") - break - } - labelMap[stringSlice[0]] = stringSlice[1] - } - } - } - - fmt.Fprintf(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) - if info.ClusterStore != "" { - fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore) - } - - if info.ClusterAdvertise != "" { - fmt.Fprintf(dockerCli.Out(), "Cluster Advertise: %s\n", info.ClusterAdvertise) - } - - if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) { - fmt.Fprintln(dockerCli.Out(), "Insecure Registries:") - for _, registry := range info.RegistryConfig.IndexConfigs { - if registry.Secure == false { - fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name) - } - } - - for _, registry := range info.RegistryConfig.InsecureRegistryCIDRs { - mask, _ := registry.Mask.Size() - fmt.Fprintf(dockerCli.Out(), " %s/%d\n", registry.IP.String(), mask) - } - } - - if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 { - fmt.Fprintln(dockerCli.Out(), "Registry Mirrors:") - for _, mirror := range info.RegistryConfig.Mirrors { - fmt.Fprintf(dockerCli.Out(), " %s\n", mirror) - } - } - - fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled) - - // Only output these warnings if the server does not support these features - if info.OSType != "windows" { - printStorageDriverWarnings(dockerCli, info) - - if !info.MemoryLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") - } - if !info.SwapLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") - } - if !info.KernelMemory { - fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support") - } - if !info.OomKillDisable { - fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") - } - if !info.CPUCfsQuota { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") - } - if !info.CPUCfsPeriod { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") - } - if !info.CPUShares { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") - } - if !info.CPUSet { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") - } - if !info.IPv4Forwarding { - fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") - } - if !info.BridgeNfIptables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") - } - if !info.BridgeNfIP6tables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") - } - } - - return nil -} - -func printStorageDriverWarnings(dockerCli *command.DockerCli, info types.Info) { - if info.DriverStatus == nil { - return - } - - for _, pair := range info.DriverStatus { - if pair[0] == "Data loop file" { - fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is strongly discouraged for production use.\n Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.\n", info.Driver) - } - if pair[0] == "Supports d_type" && pair[1] == "false" { - backingFs := getBackingFs(info) - - msg := fmt.Sprintf("WARNING: %s: the backing %s filesystem is formatted without d_type support, which leads to incorrect behavior.\n", info.Driver, backingFs) - if backingFs == "xfs" { - msg += " Reformat the filesystem with ftype=1 to enable d_type support.\n" - } - msg += " Running without d_type support will not be supported in future releases." - fmt.Fprintln(dockerCli.Err(), msg) - } - } -} - -func getBackingFs(info types.Info) string { - if info.DriverStatus == nil { - return "" - } - - for _, pair := range info.DriverStatus { - if pair[0] == "Backing Filesystem" { - return pair[1] - } - } - return "" -} - -func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) error { - tmpl, err := templates.Parse(format) - if err != nil { - return cli.StatusError{StatusCode: 64, - Status: "Template parsing error: " + err.Error()} - } - err = tmpl.Execute(dockerCli.Out(), info) - dockerCli.Out().Write([]byte{'\n'}) - return err -} diff --git a/cli/command/system/inspect.go b/cli/command/system/inspect.go deleted file mode 100644 index ad23d35a09..0000000000 --- a/cli/command/system/inspect.go +++ /dev/null @@ -1,216 +0,0 @@ -package system - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - apiclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - inspectType string - size bool - ids []string -} - -// NewInspectCommand creates a new cobra.Command for `docker inspect` -func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] NAME|ID [NAME|ID...]", - Short: "Return low-level information on Docker objects", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.ids = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.StringVar(&opts.inspectType, "type", "", "Return JSON for specified type") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - var elementSearcher inspect.GetRefFunc - switch opts.inspectType { - case "", "container", "image", "node", "network", "service", "volume", "task", "plugin", "secret": - elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) - default: - return errors.Errorf("%q is not a valid value for --type", opts.inspectType) - } - return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher) -} - -func inspectContainers(ctx context.Context, dockerCli *command.DockerCli, getSize bool) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().ContainerInspectWithRaw(ctx, ref, getSize) - } -} - -func inspectImages(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().ImageInspectWithRaw(ctx, ref) - } -} - -func inspectNetwork(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, false) - } -} - -func inspectNode(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().NodeInspectWithRaw(ctx, ref) - } -} - -func inspectService(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - // Service inspect shows defaults values in empty fields. - return dockerCli.Client().ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true}) - } -} - -func inspectTasks(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().TaskInspectWithRaw(ctx, ref) - } -} - -func inspectVolume(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().VolumeInspectWithRaw(ctx, ref) - } -} - -func inspectPlugin(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().PluginInspectWithRaw(ctx, ref) - } -} - -func inspectSecret(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().SecretInspectWithRaw(ctx, ref) - } -} - -func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { - var inspectAutodetect = []struct { - objectType string - isSizeSupported bool - isSwarmObject bool - objectInspector func(string) (interface{}, []byte, error) - }{ - { - objectType: "container", - isSizeSupported: true, - objectInspector: inspectContainers(ctx, dockerCli, getSize), - }, - { - objectType: "image", - objectInspector: inspectImages(ctx, dockerCli), - }, - { - objectType: "network", - objectInspector: inspectNetwork(ctx, dockerCli), - }, - { - objectType: "volume", - objectInspector: inspectVolume(ctx, dockerCli), - }, - { - objectType: "service", - isSwarmObject: true, - objectInspector: inspectService(ctx, dockerCli), - }, - { - objectType: "task", - isSwarmObject: true, - objectInspector: inspectTasks(ctx, dockerCli), - }, - { - objectType: "node", - isSwarmObject: true, - objectInspector: inspectNode(ctx, dockerCli), - }, - { - objectType: "plugin", - objectInspector: inspectPlugin(ctx, dockerCli), - }, - { - objectType: "secret", - isSwarmObject: true, - objectInspector: inspectSecret(ctx, dockerCli), - }, - } - - // isSwarmManager does an Info API call to verify that the daemon is - // a swarm manager. - isSwarmManager := func() bool { - info, err := dockerCli.Client().Info(ctx) - if err != nil { - fmt.Fprintln(dockerCli.Err(), err) - return false - } - return info.Swarm.ControlAvailable - } - - isErrNotSupported := func(err error) bool { - return strings.Contains(err.Error(), "not supported") - } - - return func(ref string) (interface{}, []byte, error) { - const ( - swarmSupportUnknown = iota - swarmSupported - swarmUnsupported - ) - - isSwarmSupported := swarmSupportUnknown - - for _, inspectData := range inspectAutodetect { - if typeConstraint != "" && inspectData.objectType != typeConstraint { - continue - } - if typeConstraint == "" && inspectData.isSwarmObject { - if isSwarmSupported == swarmSupportUnknown { - if isSwarmManager() { - isSwarmSupported = swarmSupported - } else { - isSwarmSupported = swarmUnsupported - } - } - if isSwarmSupported == swarmUnsupported { - continue - } - } - v, raw, err := inspectData.objectInspector(ref) - if err != nil { - if typeConstraint == "" && (apiclient.IsErrNotFound(err) || isErrNotSupported(err)) { - continue - } - return v, raw, err - } - if getSize && !inspectData.isSizeSupported { - fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.objectType) - } - return v, raw, err - } - return nil, nil, errors.Errorf("Error: No such object: %s", ref) - } -} diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go deleted file mode 100644 index 46e4316f4a..0000000000 --- a/cli/command/system/prune.go +++ /dev/null @@ -1,96 +0,0 @@ -package system - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/prune" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - all bool - filter opts.FilterOpt -} - -// NewPruneCommand creates a new cobra.Command for `docker prune` -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove unused data", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runPrune(dockerCli, opts) - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const ( - warning = `WARNING! This will remove: - - all stopped containers - - all volumes not used by at least one container - - all networks not used by at least one container - %s -Are you sure you want to continue?` - - danglingImageDesc = "- all dangling images" - allImageDesc = `- all images without at least one container associated to them` -) - -func runPrune(dockerCli *command.DockerCli, options pruneOptions) error { - var message string - - if options.all { - message = fmt.Sprintf(warning, allImageDesc) - } else { - message = fmt.Sprintf(warning, danglingImageDesc) - } - - if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { - return nil - } - - var spaceReclaimed uint64 - - for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){ - prune.RunContainerPrune, - prune.RunVolumePrune, - prune.RunNetworkPrune, - } { - spc, output, err := pruneFn(dockerCli, options.filter) - if err != nil { - return err - } - spaceReclaimed += spc - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - } - - spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter) - if err != nil { - return err - } - if spc > 0 { - spaceReclaimed += spc - fmt.Fprintln(dockerCli.Out(), output) - } - - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - - return nil -} diff --git a/cli/command/system/version.go b/cli/command/system/version.go deleted file mode 100644 index 468db7d03a..0000000000 --- a/cli/command/system/version.go +++ /dev/null @@ -1,131 +0,0 @@ -package system - -import ( - "runtime" - "time" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/dockerversion" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" -) - -var versionTemplate = `Client: - Version: {{.Client.Version}} - API version: {{.Client.APIVersion}}{{if ne .Client.APIVersion .Client.DefaultAPIVersion}} (downgraded from {{.Client.DefaultAPIVersion}}){{end}} - Go version: {{.Client.GoVersion}} - Git commit: {{.Client.GitCommit}} - Built: {{.Client.BuildTime}} - OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}} - -Server: - Version: {{.Server.Version}} - API version: {{.Server.APIVersion}} (minimum version {{.Server.MinAPIVersion}}) - Go version: {{.Server.GoVersion}} - Git commit: {{.Server.GitCommit}} - Built: {{.Server.BuildTime}} - OS/Arch: {{.Server.Os}}/{{.Server.Arch}} - Experimental: {{.Server.Experimental}}{{end}}` - -type versionOptions struct { - format string -} - -// versionInfo contains version information of both the Client, and Server -type versionInfo struct { - Client clientVersion - Server *types.Version -} - -type clientVersion struct { - Version string - APIVersion string `json:"ApiVersion"` - DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"` - GitCommit string - GoVersion string - Os string - Arch string - BuildTime string `json:",omitempty"` -} - -// ServerOK returns true when the client could connect to the docker server -// and parse the information received. It returns false otherwise. -func (v versionInfo) ServerOK() bool { - return v.Server != nil -} - -// NewVersionCommand creates a new cobra.Command for `docker version` -func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts versionOptions - - cmd := &cobra.Command{ - Use: "version [OPTIONS]", - Short: "Show the Docker version information", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runVersion(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { - ctx := context.Background() - - templateFormat := versionTemplate - if opts.format != "" { - templateFormat = opts.format - } - - tmpl, err := templates.Parse(templateFormat) - if err != nil { - return cli.StatusError{StatusCode: 64, - Status: "Template parsing error: " + err.Error()} - } - - vd := versionInfo{ - Client: clientVersion{ - Version: dockerversion.Version, - APIVersion: dockerCli.Client().ClientVersion(), - DefaultAPIVersion: dockerCli.DefaultVersion(), - GoVersion: runtime.Version(), - GitCommit: dockerversion.GitCommit, - BuildTime: dockerversion.BuildTime, - Os: runtime.GOOS, - Arch: runtime.GOARCH, - }, - } - - serverVersion, err := dockerCli.Client().ServerVersion(ctx) - if err == nil { - vd.Server = &serverVersion - } - - // first we need to make BuildTime more human friendly - t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime) - if errTime == nil { - vd.Client.BuildTime = t.Format(time.ANSIC) - } - - if vd.ServerOK() { - t, errTime = time.Parse(time.RFC3339Nano, vd.Server.BuildTime) - if errTime == nil { - vd.Server.BuildTime = t.Format(time.ANSIC) - } - } - - if err2 := tmpl.Execute(dockerCli.Out(), vd); err2 != nil && err == nil { - err = err2 - } - dockerCli.Out().Write([]byte{'\n'}) - return err -} diff --git a/cli/command/task/print.go b/cli/command/task/print.go deleted file mode 100644 index 3df3b2985a..0000000000 --- a/cli/command/task/print.go +++ /dev/null @@ -1,84 +0,0 @@ -package task - -import ( - "fmt" - "sort" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" -) - -type tasksBySlot []swarm.Task - -func (t tasksBySlot) Len() int { - return len(t) -} - -func (t tasksBySlot) Swap(i, j int) { - t[i], t[j] = t[j], t[i] -} - -func (t tasksBySlot) Less(i, j int) bool { - // Sort by slot. - if t[i].Slot != t[j].Slot { - return t[i].Slot < t[j].Slot - } - - // If same slot, sort by most recent. - return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) -} - -// Print task information in a format. -// Besides this, command `docker node ps ` -// and `docker stack ps` will call this, too. -func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error { - sort.Stable(tasksBySlot(tasks)) - - names := map[string]string{} - nodes := map[string]string{} - - tasksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewTaskFormat(format, quiet), - Trunc: trunc, - } - - prevName := "" - for _, task := range tasks { - serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) - if err != nil { - return err - } - - nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID) - if err != nil { - return err - } - - name := "" - if task.Slot != 0 { - name = fmt.Sprintf("%v.%v", serviceName, task.Slot) - } else { - name = fmt.Sprintf("%v.%v", serviceName, task.NodeID) - } - - // Indent the name if necessary - indentedName := name - if name == prevName { - indentedName = fmt.Sprintf(" \\_ %s", indentedName) - } - prevName = name - - names[task.ID] = name - if tasksCtx.Format.IsTable() { - names[task.ID] = indentedName - } - nodes[task.ID] = nodeValue - } - - return formatter.TaskWrite(tasksCtx, tasks, names, nodes) -} diff --git a/cli/command/trust.go b/cli/command/trust.go deleted file mode 100644 index c0742bc5b2..0000000000 --- a/cli/command/trust.go +++ /dev/null @@ -1,43 +0,0 @@ -package command - -import ( - "os" - "strconv" - - "github.com/spf13/pflag" -) - -var ( - // TODO: make this not global - untrusted bool -) - -// AddTrustVerificationFlags adds content trust flags to the provided flagset -func AddTrustVerificationFlags(fs *pflag.FlagSet) { - trusted := getDefaultTrustState() - fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image verification") -} - -// AddTrustSigningFlags adds "signing" flags to the provided flagset -func AddTrustSigningFlags(fs *pflag.FlagSet) { - trusted := getDefaultTrustState() - fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image signing") -} - -// getDefaultTrustState returns true if content trust is enabled through the $DOCKER_CONTENT_TRUST environment variable. -func getDefaultTrustState() bool { - var trusted bool - if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { - if t, err := strconv.ParseBool(e); t || err != nil { - // treat any other value as true - trusted = true - } - } - return trusted -} - -// IsTrusted returns true if content trust is enabled, either through the $DOCKER_CONTENT_TRUST environment variable, -// or through `--disabled-content-trust=false` on a command. -func IsTrusted() bool { - return !untrusted -} diff --git a/cli/command/utils.go b/cli/command/utils.go deleted file mode 100644 index 853fe11c78..0000000000 --- a/cli/command/utils.go +++ /dev/null @@ -1,119 +0,0 @@ -package command - -import ( - "bufio" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/system" -) - -// CopyToFile writes the content of the reader to the specified file -func CopyToFile(outfile string, r io.Reader) error { - // We use sequential file access here to avoid depleting the standby list - // on Windows. On Linux, this is a call directly to ioutil.TempFile - tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".docker_temp_") - if err != nil { - return err - } - - tmpPath := tmpFile.Name() - - _, err = io.Copy(tmpFile, r) - tmpFile.Close() - - if err != nil { - os.Remove(tmpPath) - return err - } - - if err = os.Rename(tmpPath, outfile); err != nil { - os.Remove(tmpPath) - return err - } - - return nil -} - -// capitalizeFirst capitalizes the first character of string -func capitalizeFirst(s string) string { - switch l := len(s); l { - case 0: - return s - case 1: - return strings.ToLower(s) - default: - return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) - } -} - -// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. -func PrettyPrint(i interface{}) string { - switch t := i.(type) { - case nil: - return "None" - case string: - return capitalizeFirst(t) - default: - return capitalizeFirst(fmt.Sprintf("%s", t)) - } -} - -// PromptForConfirmation requests and checks confirmation from user. -// This will display the provided message followed by ' [y/N] '. If -// the user input 'y' or 'Y' it returns true other false. If no -// message is provided "Are you sure you want to proceed? [y/N] " -// will be used instead. -func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool { - if message == "" { - message = "Are you sure you want to proceed?" - } - message += " [y/N] " - - fmt.Fprintf(outs, message) - - // On Windows, force the use of the regular OS stdin stream. - if runtime.GOOS == "windows" { - ins = NewInStream(os.Stdin) - } - - reader := bufio.NewReader(ins) - answer, _, _ := reader.ReadLine() - return strings.ToLower(string(answer)) == "y" -} - -// PruneFilters returns consolidated prune filters obtained from config.json and cli -func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { - if dockerCli.ConfigFile() == nil { - return pruneFilters - } - for _, f := range dockerCli.ConfigFile().PruneFilters { - parts := strings.SplitN(f, "=", 2) - if len(parts) != 2 { - continue - } - if parts[0] == "label" { - // CLI label filter supersede config.json. - // If CLI label filter conflict with config.json, - // skip adding label! filter in config.json. - if pruneFilters.Include("label!") && pruneFilters.ExactMatch("label!", parts[1]) { - continue - } - } else if parts[0] == "label!" { - // CLI label! filter supersede config.json. - // If CLI label! filter conflict with config.json, - // skip adding label filter in config.json. - if pruneFilters.Include("label") && pruneFilters.ExactMatch("label", parts[1]) { - continue - } - } - pruneFilters.Add(parts[0], parts[1]) - } - - return pruneFilters -} diff --git a/cli/command/volume/client_test.go b/cli/command/volume/client_test.go deleted file mode 100644 index c29655cdb0..0000000000 --- a/cli/command/volume/client_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) - volumeInspectFunc func(volumeID string) (types.Volume, error) - volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) - volumeRemoveFunc func(volumeID string, force bool) error - volumePruneFunc func(filter filters.Args) (types.VolumesPruneReport, error) -} - -func (c *fakeClient) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) { - if c.volumeCreateFunc != nil { - return c.volumeCreateFunc(options) - } - return types.Volume{}, nil -} - -func (c *fakeClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) { - if c.volumeInspectFunc != nil { - return c.volumeInspectFunc(volumeID) - } - return types.Volume{}, nil -} - -func (c *fakeClient) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) { - if c.volumeListFunc != nil { - return c.volumeListFunc(filter) - } - return volumetypes.VolumesListOKBody{}, nil -} - -func (c *fakeClient) VolumesPrune(ctx context.Context, filter filters.Args) (types.VolumesPruneReport, error) { - if c.volumePruneFunc != nil { - return c.volumePruneFunc(filter) - } - return types.VolumesPruneReport{}, nil -} - -func (c *fakeClient) VolumeRemove(ctx context.Context, volumeID string, force bool) error { - if c.volumeRemoveFunc != nil { - return c.volumeRemoveFunc(volumeID, force) - } - return nil -} diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go deleted file mode 100644 index 9086c99248..0000000000 --- a/cli/command/volume/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewVolumeCommand returns a cobra command for `volume` subcommands -func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "volume COMMAND", - Short: "Manage volumes", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.21"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/volume/create.go b/cli/command/volume/create.go deleted file mode 100644 index 8392cf0029..0000000000 --- a/cli/command/volume/create.go +++ /dev/null @@ -1,70 +0,0 @@ -package volume - -import ( - "fmt" - - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type createOptions struct { - name string - driver string - driverOpts opts.MapOpts - labels opts.ListOpts -} - -func newCreateCommand(dockerCli command.Cli) *cobra.Command { - opts := createOptions{ - driverOpts: *opts.NewMapOpts(nil, nil), - labels: opts.NewListOpts(opts.ValidateEnv), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] [VOLUME]", - Short: "Create a volume", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 1 { - if opts.name != "" { - return errors.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") - } - opts.name = args[0] - } - return runCreate(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.StringVarP(&opts.driver, "driver", "d", "local", "Specify volume driver name") - flags.StringVar(&opts.name, "name", "", "Specify volume name") - flags.Lookup("name").Hidden = true - flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.Var(&opts.labels, "label", "Set metadata for a volume") - - return cmd -} - -func runCreate(dockerCli command.Cli, opts createOptions) error { - client := dockerCli.Client() - - volReq := volumetypes.VolumesCreateBody{ - Driver: opts.driver, - DriverOpts: opts.driverOpts.GetAll(), - Name: opts.name, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - } - - vol, err := client.VolumeCreate(context.Background(), volReq) - if err != nil { - return err - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", vol.Name) - return nil -} diff --git a/cli/command/volume/create_test.go b/cli/command/volume/create_test.go deleted file mode 100644 index 45cf631134..0000000000 --- a/cli/command/volume/create_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package volume - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestVolumeCreateErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) - expectedError string - }{ - { - args: []string{"volumeName"}, - flags: map[string]string{ - "name": "volumeName", - }, - expectedError: "Conflicting options: either specify --name or provide positional arg, not both", - }, - { - args: []string{"too", "many"}, - expectedError: "requires at most 1 argument(s)", - }, - { - volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) { - return types.Volume{}, errors.Errorf("error creating volume") - }, - expectedError: "error creating volume", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newCreateCommand( - test.NewFakeCli(&fakeClient{ - volumeCreateFunc: tc.volumeCreateFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeCreateWithName(t *testing.T) { - name := "foo" - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { - if body.Name != name { - return types.Volume{}, errors.Errorf("expected name %q, got %q", name, body.Name) - } - return types.Volume{ - Name: body.Name, - }, nil - }, - }, buf) - - // Test by flags - cmd := newCreateCommand(cli) - cmd.Flags().Set("name", name) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) - - // Then by args - buf.Reset() - cmd = newCreateCommand(cli) - cmd.SetArgs([]string{name}) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) -} - -func TestVolumeCreateWithFlags(t *testing.T) { - expectedDriver := "foo" - expectedOpts := map[string]string{ - "bar": "1", - "baz": "baz", - } - expectedLabels := map[string]string{ - "lbl1": "v1", - "lbl2": "v2", - } - name := "banana" - - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { - if body.Name != "" { - return types.Volume{}, errors.Errorf("expected empty name, got %q", body.Name) - } - if body.Driver != expectedDriver { - return types.Volume{}, errors.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) - } - if !compareMap(body.DriverOpts, expectedOpts) { - return types.Volume{}, errors.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) - } - if !compareMap(body.Labels, expectedLabels) { - return types.Volume{}, errors.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) - } - return types.Volume{ - Name: name, - }, nil - }, - }, buf) - - cmd := newCreateCommand(cli) - cmd.Flags().Set("driver", "foo") - cmd.Flags().Set("opt", "bar=1") - cmd.Flags().Set("opt", "baz=baz") - cmd.Flags().Set("label", "lbl1=v1") - cmd.Flags().Set("label", "lbl2=v2") - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) -} - -func compareMap(actual map[string]string, expected map[string]string) bool { - if len(actual) != len(expected) { - return false - } - for key, value := range actual { - if expectedValue, ok := expected[key]; ok { - if expectedValue != value { - return false - } - } else { - return false - } - } - return true -} diff --git a/cli/command/volume/inspect.go b/cli/command/volume/inspect.go deleted file mode 100644 index 70db264951..0000000000 --- a/cli/command/volume/inspect.go +++ /dev/null @@ -1,45 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - names []string -} - -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] VOLUME [VOLUME...]", - Short: "Display detailed information on one or more volumes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - - ctx := context.Background() - - getVolFunc := func(name string) (interface{}, []byte, error) { - i, err := client.VolumeInspect(ctx, name) - return i, nil, err - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) -} diff --git a/cli/command/volume/inspect_test.go b/cli/command/volume/inspect_test.go deleted file mode 100644 index bc1b526440..0000000000 --- a/cli/command/volume/inspect_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package volume - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestVolumeInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeInspectFunc func(volumeID string) (types.Volume, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - return types.Volume{}, errors.Errorf("error while inspecting the volume") - }, - expectedError: "error while inspecting the volume", - }, - { - args: []string{"foo"}, - flags: map[string]string{ - "format": "{{invalid format}}", - }, - expectedError: "Template parsing error", - }, - { - args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - if volumeID == "foo" { - return types.Volume{ - Name: "foo", - }, nil - } - return types.Volume{}, errors.Errorf("error while inspecting the volume") - }, - expectedError: "error while inspecting the volume", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeInspectWithoutFormat(t *testing.T) { - testCases := []struct { - name string - args []string - volumeInspectFunc func(volumeID string) (types.Volume, error) - }{ - { - name: "single-volume", - args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - if volumeID != "foo" { - return types.Volume{}, errors.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) - } - return *Volume(), nil - }, - }, - { - name: "multiple-volume-with-labels", - args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - return *Volume(VolumeName(volumeID), VolumeLabels(map[string]string{ - "foo": "bar", - })), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-without-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} - -func TestVolumeInspectWithFormat(t *testing.T) { - volumeInspectFunc := func(volumeID string) (types.Volume, error) { - return *Volume(VolumeLabels(map[string]string{ - "foo": "bar", - })), nil - } - testCases := []struct { - name string - format string - args []string - volumeInspectFunc func(volumeID string) (types.Volume, error) - }{ - { - name: "simple-template", - format: "{{.Name}}", - args: []string{"foo"}, - volumeInspectFunc: volumeInspectFunc, - }, - { - name: "json-template", - format: "{{json .Labels}}", - args: []string{"foo"}, - volumeInspectFunc: volumeInspectFunc, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.Flags().Set("format", tc.format) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-with-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/volume/list.go b/cli/command/volume/list.go deleted file mode 100644 index 3577db9554..0000000000 --- a/cli/command/volume/list.go +++ /dev/null @@ -1,73 +0,0 @@ -package volume - -import ( - "sort" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type byVolumeName []*types.Volume - -func (r byVolumeName) Len() int { return len(r) } -func (r byVolumeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byVolumeName) Less(i, j int) bool { - return r[i].Name < r[j].Name -} - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List volumes", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") - flags.StringVar(&opts.format, "format", "", "Pretty-print volumes using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'dangling=true')") - - return cmd -} - -func runList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - volumes, err := client.VolumeList(context.Background(), opts.filter.Value()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().VolumesFormat - } else { - format = formatter.TableFormatKey - } - } - - sort.Sort(byVolumeName(volumes.Volumes)) - - volumeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewVolumeFormat(format, opts.quiet), - } - return formatter.VolumeWrite(volumeCtx, volumes.Volumes) -} diff --git a/cli/command/volume/list_test.go b/cli/command/volume/list_test.go deleted file mode 100644 index 4f5e99389f..0000000000 --- a/cli/command/volume/list_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package volume - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestVolumeListErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) - expectedError string - }{ - { - args: []string{"foo"}, - expectedError: "accepts no argument", - }, - { - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{}, errors.Errorf("error listing volumes") - }, - expectedError: "error listing volumes", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newListCommand( - test.NewFakeCli(&fakeClient{ - volumeListFunc: tc.volumeListFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeListWithoutFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-without-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestVolumeListWithConfigFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - VolumesFormat: "{{ .Name }} {{ .Driver }} {{ .Labels }}", - }) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-with-config-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestVolumeListWithFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.Flags().Set("format", "{{ .Name }} {{ .Driver }} {{ .Labels }}") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-with-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go deleted file mode 100644 index f7d823ffac..0000000000 --- a/cli/command/volume/prune.go +++ /dev/null @@ -1,78 +0,0 @@ -package volume - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all unused volumes", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'label=